Resumen teórico
Cuando le pides a un amigo que te pase la sal, los dos siguen reglas escondidas: tú dices por favor, él te mira, te la entrega y tú das las gracias. Si uno de los dos rompiera las reglas, la sal nunca llegaría. Los computadores también necesitan reglas así. Un protocolo es un conjunto de reglas que dice quién habla primero, cómo es un mensaje y qué significa cada respuesta. Como todos los programas siguen las mismas reglas, un navegador escrito en un país puede conversar con un servidor escrito por otra persona en otro país.
Esta área trata de los protocolos que usas cada vez que abres una página, ves un video, chateas o llamas al servidor de una aplicación. Empezamos por HTTP, el idioma de la web: sus métodos, sus respuestas numeradas llamadas códigos de estado y sus notas llamadas encabezados (headers). Luego vemos cómo la web ahorra tiempo con cachés, cómo las cookies te recuerdan y cómo los navegadores te protegen con CORS. Después viene TLS, el candado que guarda los secretos, y las tres versiones de HTTP (1.1, 2 y 3), que llevan los mismos mensajes de maneras cada vez más rápidas.
Encima de HTTP la gente construye APIs, que son menús de cosas que un programa puede pedirle a otro programa. Conocerás los estilos REST, GraphQL, JSON-RPC y gRPC, y también WebSocket y los server-sent events, que dejan que el servidor te hable sin que le preguntes primero. Al final sabrás para qué sirve cada uno y cuánto cuesta cada uno, que es exactamente lo que pregunta el quiz.
El panorama: conversar por la red
Piensa en un restaurante. Tú eres el cliente (client): pides cosas. El mesero lleva el pedido a la cocina, que es el servidor (server): lo prepara y lo manda de vuelta. En la web es igual. El navegador envía una solicitud (request: 'dame esta página') y el servidor envía una respuesta (response: 'aquí está'). La conversación siempre empieza por el cliente.
Los mensajes no vuelan por arte de magia. Pasan por capas, como una carta que va dentro de un sobre, que va dentro de una bolsa, que va en un camión. Cada capa tiene su trabajo.
tu programa 'GET /foto' <- HTTP (esta área)
|
cifrado revuelve los bytes <- TLS
|
transporte entrega los bytes <- TCP o QUIC (UDP)
|
la red lleva paquetes <- IP, cables, Wi-Fi| Palabra | Significado |
|---|---|
| Protocolo | Un conjunto de reglas para una conversación entre programas. |
| TCPun protocolo de transporte que entrega los bytes en orden y reenvía los que se pierden | Confiable, pero te hace esperar por la primera pieza que falta. |
| UDPun transporte muy simple que solo envía paquetes, sin prometer que lleguen | Rápido y simple. QUIC se construye encima de él. |
| Round trip (RTT)el tiempo que tarda un mensaje en ir al otro lado y volver la respuesta | La unidad que usamos para contar qué tan lento es un handshake. |
| Latencia | El retraso de la red. Latencia alta significa que cada round trip es lento. |
Semántica de HTTP: métodos, códigos de estado y encabezados
HTTP (HyperText Transfer Protocol) es el idioma de la web. Todo mensaje HTTP tiene tres partes: una primera línea que dice qué se pide (o cómo fue), luego los encabezados (headers), que son notas pequeñas, y por último un cuerpo (body) opcional, que es el contenido de verdad, como una foto o un texto.
GET /foto.png HTTP/1.1
Host: ejemplo.test
Accept: image/png
- Toda línea termina con dos caracteres invisibles, CR y LF (escritos
\r\n). Una línea vacía significa 'los encabezados terminaron'. - La primera línea de una respuesta es la status line: la versión, un código de estado de tres dígitos y un texto corto.
- En HTTP/1.1 el encabezado
Hostes obligatorio. Un computador puede alojar muchos sitios, yHostdice cuál sitio quieres. Sin él (o con dos), el servidor debe responder400 Bad Request.
Métodos: los verbos
El método dice qué quieres hacer. Las reglas oficiales (RFC 9110) describen tres propiedades. Un método es seguro (safe) si solo lee y no pide ningún cambio. Es idempotente si hacerlo muchas veces tiene el mismo efecto que hacerlo una vez. Es cacheable si la respuesta puede guardarse para reutilizarla.
| Método | Qué hace | Seguro | Idempotente |
|---|---|---|---|
| GET | Lee un recurso | sí | sí |
| HEAD | Igual que GET, pero solo vuelven los encabezados, sin cuerpo | sí | sí |
| OPTIONS | Pregunta qué está permitido | sí | sí |
| PUT | Reemplaza un recurso por lo que envías | no | sí |
| DELETE | Elimina un recurso | no | sí |
| POST | Envía algo para procesar, a menudo para crear | no | no |
| PATCH | Cambia una parte de un recurso | no | no |
Idempotente habla del efecto, no de la respuesta. El primer DELETE puede responder 204 y el segundo 404, y el método sigue siendo idempotente, porque el estado final es el mismo: la cosa ya no está.
Códigos de estado: cómo fue
El primer dígito dice la familia: 1xx información, 2xx éxito, 3xx ve a otro lugar, 4xx el cliente se equivocó, 5xx el servidor se equivocó.
| Código | Significado | Cuándo |
|---|---|---|
| 200 / 201 / 204 | OK / Created / No Content | Created viene con un encabezado Location con la nueva URL |
| 206 | Partial Content | Respuesta a una solicitud Range (reanudar una descarga) |
| 301, 302, 303, 307, 308 | Redirecciones | 301 y 308 son permanentes; 302 y 307, temporales. 307 y 308 conservan el método y el cuerpo; 303 siempre significa 'ahora haz un GET en otra página' |
| 304 | Not Modified | Tu copia guardada sigue siendo buena |
| 400 | Bad Request | El mensaje está roto (línea mala, falta Host) |
| 401 | Unauthorized | En realidad significa 'no autenticado': dime quién eres. Viene con WWW-Authenticate |
| 403 | Forbidden | Sé quién eres, y la respuesta es no |
| 404 / 405 | Not Found / Method Not Allowed | No existe esa ruta / la ruta existe, pero no para ese método |
| 406 / 415 | Not Acceptable / Unsupported Media Type | El servidor no puede dar el formato del Accept / el servidor no acepta el Content-Type que enviaste |
| 409 / 412 | Conflict / Precondition Failed | Choca con el estado actual / una condición If-Match era falsa |
| 413 / 414 / 431 | Demasiado grande | El cuerpo / la URL / los encabezados son demasiado grandes |
| 422 | Unprocessable Content | Formato correcto, pero el contenido rompe una regla |
| 500 / 501 / 505 | Error del servidor | Bug / no implementado / versión de HTTP no soportada |
¿Dónde termina el cuerpo?
En HTTP/1.1 la conexión queda abierta (esto se llama keep-alive y es lo normal: abrir una conexión cuesta un round trip, así que la reutilizamos). El siguiente mensaje llega justo después del primero, así que quien lee debe saber exactamente dónde para cada cuerpo. Hay dos maneras.
| Pregunta | Content-Length | Transfer-Encoding: chunked |
|---|---|---|
| Cuándo usar | El tamaño se conoce antes de enviar | El tamaño aún no se conoce (un flujo) |
| Cómo termina | Leer exactamente esa cantidad de bytes | Chunks, cada uno con su tamaño en hexadecimalcontar en base 16, con los dígitos del 0 al 9 y de la a a la f; así b vale 11; un chunk de tamaño 0 lo cierra |
| En el cable | hello world | b\r\nhello world\r\n0\r\n\r\n |
Si un mensaje tiene ambos encabezados, Transfer-Encoding gana y el servidor debería rechazarlo con 400. ¿Por qué? Si un proxy cree en uno y el servidor detrás de él cree en el otro, los dos discrepan sobre dónde termina la solicitud, y los bytes que sobran se leen como una solicitud nueva. Ese ataque se llama request smuggling. Cerrar la conexión al final del cuerpo también funciona, pero entonces no se puede reutilizar.
Encabezados útiles
Content-Typedescribe el cuerpo que estás enviando;Acceptdice qué formatos estás dispuesto a recibir.ETages una etiqueta (una huella digital) de la versión actual de un recurso. Devolverla enIf-Matchen un PUT significa 'escribe solo si nadie cambió esto desde que lo leí'. Si alguien lo cambió, el servidor responde412. Esto evita la actualización perdida (lost update), en la que dos personas editan y la segunda borra el trabajo de la primera.Range: bytes=1000-pide el resto de un archivo.If-Range: "a"añade: 'solo si la versión sigue siendo"a"'. Si lo es, recibes206con la parte; si el archivo cambió, recibes200con el archivo nuevo completo.
TLS: el candado de la conexión
HTTP normal viaja en texto plano. Cualquiera en el camino (el dueño de un Wi-Fi, un router) podría leer o cambiar el mensaje. TLS (Transport Layer Security) envuelve la conversación en un candado. HTTP dentro de TLS es HTTPS. Antes de que se mueva cualquier dato de la web, los dos lados hacen un baile corto llamado handshake.
Un handshake terminado da tres cosas: los dos acuerdan una versión y los métodos para revolver secretos, el cliente autentica al servidor (comprueba que es quien dice ser) y ambos lados quedan con la misma clave secreta, que nadie más conoce. Hacer que el cliente se pruebe con un certificado también existe, pero en la web es opcional y raro.
Dos maneras de guardar un secreto
| Tipo | Asimétrica | Simétrica |
|---|---|---|
| Idea | Una clave pública y una clave privada que van juntas | Una sola clave compartida por los dos lados |
| Velocidad | Lenta y cara | Muy rápida |
| Se usa en TLS para | Firmar (probar identidad) y Diffie-Hellmanun truco matemático ingenioso que deja que dos lados acuerden un número secreto hablando en público (acordar un secreto) durante el handshake | Proteger todos los datos después, con un modo AEAD que da secreto y detección de manipulación a la vez |
Cómo confía el cliente en el servidor
El servidor muestra un certificado: una cédula de identidad con su nombre y su clave pública, firmada por una autoridad certificadora (CA). Normalmente la cadena tiene tres eslabones: una CA raíz firma una intermedia, que firma el certificado del sitio. Tu sistema operativo o navegador ya trae una lista de raíces de confianza. El cliente comprueba las firmas subiendo por la cadena, que las fechas sean válidas y que el nombre del certificado coincida con el host que quería.
Extensiones útiles en el primer mensaje (ClientHello)
- SNI (Server Name Indication): el cliente escribe el nombre del host en el primer mensaje. El servidor lo necesita para elegir el certificado correcto antes de leer cualquier HTTP, así que no puede esperar al encabezado
Host. Es el virtual hosting, una capa más abajo. - ALPN: el cliente lista los protocolos de aplicación que habla (
h2,http/1.1) y el servidor elige uno. No cuesta ningún round trip extra. Si el servidor no conoce ALPN, la conexión simplemente sigue en HTTP/1.1.
¿Cuántos round trips?
Aquí contamos después del handshake de TCP, sin reanudación de sesión. TLS 1.2 necesita 2 round trips. TLS 1.3 necesita solo 1: el cliente adivina el método de claves y ya envía su parte (key_share) en el primer mensaje, así que el servidor puede responder todo de una vez.
TLS 1.3 TLS 1.2
cliente servidor cliente servidor
|-- ClientHello + clave->| |-- ClientHello ------->|
|<-- ServerHello, cert, | |<-- ServerHello, cert -| round trip 1
| Finished -----------| |-- intercambio claves->|
|-- Finished + datos --->| |<-- Finished ----------| round trip 2
| (1 round trip) | |-- datos ------------->|Forward secrecy y 0-RTT
Imagina que un espía graba tu conversación cifrada durante meses y después roba la clave privada del servidor. Con forward secrecy (secreto hacia adelante) las conversaciones antiguas siguen seguras, porque cada conexión usó un par Diffie-Hellman nuevo y desechable, borrado después, y la clave del certificado solo firma. El TLS 1.2 antiguo con intercambio de claves RSA estático no tenía esto: clave robada, todas las grabaciones legibles. TLS 1.3 eliminó el RSA estático y volvió obligatorio el forward secrecy.
Cuando te reconectas, TLS 1.3 puede usar un secreto guardado (un PSK, que viene de un ticket) y enviar datos ya en el primer vuelo. Es 0-RTT, o early data. Es más rápido, pero el servidor no puede añadir un valor nuevo, así que un atacante puede repetir (replay) ese primer vuelo. El early data tampoco tiene forward secrecy. Por eso solo debe llevar solicitudes que sean seguras de repetir.
↑ Volver al índiceHTTP/2: muchas solicitudes en una conexión
HTTP/1.1 tiene un problema de fila: una conexión lleva una solicitud a la vez. Para ir más rápido, los navegadores abren unas seis conexiones por origen. Una página con 200 imágenes pequeñas necesita entonces 34 rondas de seis, y cada ronda cuesta un round trip. Con 50 ms de retraso eso suma segundos.
Frames, streams y mensajes
HTTP/2 mantiene el mismo significado (métodos, códigos de estado, encabezados) y cambia solo cómo se envía. Es binario, no texto. La pieza más pequeña es el frame (trama), con un encabezado fijo de 9 bytes que tiene longitud, tipo, flags y un número de stream. Un stream es un canal lógico de dos vías dentro de la conexión. Un mensaje (una solicitud o una respuesta) es el grupo de frames de un stream. Los frames de streams distintos se mezclan, lo que se llama multiplexación.
una conexión TCP:
[S1 HEADERS][S3 HEADERS][S1 DATA][S5 HEADERS][S3 DATA][S1 DATA]...
^ stream 1 ^ stream 3 ... frames de varios streams intercalados- Los números de stream tienen 31 bits. El cliente usa números impares, el servidor usa pares, y el 0 es para la conexión misma. Solo crecen y nunca se reutilizan.
- HTTP/2 no tiene request line ni status line. Se vuelven pseudo-encabezados que empiezan con dos puntos:
:method,:scheme,:authority(reemplaza aHost),:pathy:statusen la respuesta. - Tipos de frame:
HEADERSyDATAllevan el mensaje.RST_STREAMcancela un stream (en HTTP/1.1 había que cerrar la conexión).GOAWAYcierra la conexión.SETTINGS,PINGyWINDOW_UPDATEse ocupan de configuraciones, de comprobar que el otro lado sigue vivo y del control de flujo. - Empiezas HTTP/2 sobre TLS usando ALPN con el identificador
h2.h2ces HTTP/2 sin TLS, usado entre servicios internos, no por navegadores.
HPACK: encoger los encabezados
Los encabezados se repiten mucho: el mismo user-agent y la misma cookie van en cada solicitud. HTTP/2 los comprime con HPACK. Mantiene dos tablas. La tabla estática tiene 61 entradas fijas para encabezados muy comunes, como :method: GET. La tabla dinámica empieza vacía y se llena con los encabezados vistos en esta conexión (los más antiguos salen cuando se llena). Los dos lados mantienen la misma tabla, así que un encabezado ya visto cuesta solo un pequeño número de índice.
¿Por qué no usar simplemente gzip, como hacía SPDY, más antiguo? Porque comprimir un secreto (como una cookie) junto con texto que controla el atacante filtra información por el tamaño. Es el ataque CRIME. HPACK nunca busca repeticiones parciales dentro de un valor: reemplaza el encabezado completo por un índice o usa un código de Huffmanuna tabla que escribe los símbolos comunes con pocos bits y los raros con muchos fijo, y puede marcar campos sensibles como 'nunca indexar'.
Control de flujo
El control de flujo (flow control) evita que un emisor rápido ahogue a un receptor lento. Vale solo para frames DATA y funciona con dos ventanas: una para cada stream y una para toda la conexión. Ambas empiezan en 65.535 bytes. Cada byte enviado se resta de las dos. El receptor devuelve crédito con WINDOW_UPDATE. El emisor puede enviar como máximo el menor de los dos saldos. (La prioridad es otra cosa: solo una pista sobre qué stream atender primero.)
ventana de la conexión: 65535
stream 1 envía 16384 + 16384 + 7232 = 40000 bytes
ventana de la conexión restante: 25535
ventana del stream 3 (nunca usado): 65535
stream 3 puede enviar: min(65535, 25535) = 25535 bytesLo que no funcionó
- Server push: el servidor enviaba recursos que el cliente aún no había pedido. Pero el servidor no sabe qué tiene ya el cliente en su cache, así que a menudo desperdiciaba datos. Los navegadores lo eliminaron. El reemplazo es
103 Early Hintsyrel=preload, donde decide el navegador. - Domain sharding (repartir imágenes en varios nombres de host, un truco de HTTP/1.1) ahora estorba: cada host nuevo cuesta una consulta de DNSla guía telefónica de internet, que convierte un nombre como ejemplo.test en una dirección, un handshake TCP y TLS y un nuevo slow start, y rompe la única conexión ya calentada y la única tabla HPACK.
- Head-of-line blocking en TCP: HTTP/2 arregló la fila en la aplicación, pero todos los streams comparten un único flujo de bytes ordenado de TCP. Si se pierde un paquete, TCP retiene todo lo que vino después, incluso frames de streams que no perdieron nada. En un enlace con pérdidas, una conexión HTTP/2 puede ser peor que seis conexiones HTTP/1.1.
- HTTP/1.1 (6 conexiones)
- 34 rondas
- HTTP/2 (1 conexión)
- 1 rondas
HTTP/3 y QUIC
Para quitar el problema de la fila única de TCP, Internet construyó un transporte nuevo llamado QUIC. Corre encima de UDP, pero UDP es solo un sobre que cruza la red que ya existe. Todo lo demás vive dentro de QUIC: streams, reenvío de datos perdidos, control de flujo, control de congestiónbajar la velocidad cuando la red parece llena, para que el emisor no empeore las cosas y cifrado. Como QUIC suele correr en el espacio de usuario (dentro del navegador o del servidor, no en el sistema operativo), evoluciona con una actualización de la aplicación. El costo: usa más CPU por paquete que el TCP del kernel. HTTP/3 es HTTP llevado sobre QUIC.
| Característica | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Transporte | TCP | TCP | QUIC sobre UDP |
| Formato | Texto | Frames binarios | Frames binarios |
| Solicitudes a la vez en una conexión | 1 | Muchas | Muchas |
| Compresión de encabezados | ninguna | HPACK | QPACK |
| Round trips antes de la primera solicitud (TLS 1.3, primera visita) | 2 | 2 | 1 |
| Un paquete perdido retrasa | su conexión | todos los streams | solo su propio stream |
Tres grandes ganancias
- Sin head-of-line blocking en el transporte. Cada stream de QUIC se ordena por sí mismo. Cuando se pierde un paquete, solo esperan los streams cuyos datos llevaba. Los demás siguen llegando.
- Un handshake más corto. Con TCP, primero va TCP (1 round trip) y luego TLS 1.3 (1 round trip): 2 en total antes de la primera solicitud HTTP. QUIC lleva el handshake de TLS 1.3 dentro de sus primeros paquetes: 1. (Contado para una primera visita, sin reanudación y sin TCP Fast Open. En una conexión reanudada ambos pueden usar 0-RTT, con el riesgo de replay que viste en la sección de TLS.)
- Migración de conexión. Una conexión TCP está atada a cuatro cosas: dos direcciones IP y dos puertos. Si tu teléfono pasa del Wi-Fi a los datos móviles, la IP cambia y TCP se pierde. QUIC usa connection IDs que van en los paquetes, así que el servidor comprueba el nuevo camino y sigue con las mismas claves y los mismos streams.
TCP + TLS 1.3 QUIC
cliente servidor cliente servidor
|--SYN--------->| |--Initial+ClientHello->|
|<--SYN/ACK-----| 1 RTT |<--Initial+ServerHello-| 1 RTT
|--ACK+Hello--->| |--Finished + request-->|
|<--ServerHello-| 2 RTT |
|--Finished+GET>|
No existe QUIC sin cifrado: el handshake de TLS 1.3 es parte de conectarse, y las versiones anteriores de TLS no se aceptan. QUIC también cifra casi toda su propia información de control (números de paquete, confirmaciones). La razón es la osificación (ossification): cuando las cajas en medio de Internet se acostumbran a leer el formato de un protocolo, rompen cualquier cambio, incluso uno válido. TCP sufrió esto porque su encabezado va en claro. QUIC deja visible solo lo mínimo, como el connection ID, para que la red no dependa del resto.
QPACK, Alt-Svc y fallback
- QPACK reemplaza a HPACK. La tabla de HPACK la modifican los propios bloques de encabezados, así que deben decodificarse en el orden exacto, pero QUIC solo ordena los datos dentro de cada stream. QPACK envía las actualizaciones de la tabla en un stream aparte, y cada bloque dice de qué entradas depende, así el decodificador sabe si debe esperar.
- Descubrir que existe HTTP/3. Una dirección
https://no dice la versión, así que el navegador empieza por TCP. El servidor entonces anunciaalt-svc: h3=":443"; ma=86400(protocolo, puerto UDP, por cuánto tiempo vale). Otra vía es un registro DNS especial de tipo HTTPS. Entonces la siguiente conexión puede usar QUIC. - Es solo una pista. Algunas redes bloquean UDP. El navegador prueba QUIC y, si no funciona, usa la conexión TCP (HTTP/2 o HTTP/1.1), normalmente compitiendo ambas en paralelo para que el usuario no espere un timeout.
- TCP + TLS 1.3 (HTTP/1.1 o HTTP/2)
- 2 round trips
- QUIC (HTTP/3)
- 1 round trips
¿Cuándo es mejor HTTP/3? Cuando la latencia es alta (el handshake ahorra un round trip) o cuando hay pérdida de paquetes (los streams no se bloquean entre sí), como en las redes móviles. En redes rápidas y limpias HTTP/2 y HTTP/3 rinden más o menos igual, y QUIC en el espacio de usuario puede incluso gastar más CPU.
↑ Volver al índiceREST: una API hecha de cosas
Una API es un menú de cosas que un programa puede pedirle a otro programa. REST es un estilo para construir APIs web, descrito por Roy Fielding en su tesis doctoral (2000). La idea principal: no pienses en acciones, piensa en cosas (llamadas recursos), como libros, pedidos o usuarios. Cada cosa tiene una dirección, una URL.
| Quiero... | Solicitud | Respuesta |
|---|---|---|
| Leer el libro 42 | GET /books/42 | 200 OK y el libro |
| Crear un libro | POST /books | 201 Created con un encabezado Location con la nueva URL |
| Reemplazar el libro 42 | PUT /books/42 (el libro completo) | 200 o 204 |
| Cambiar un campo | PATCH /books/42 (solo los cambios) | 200 o 204 |
| Eliminar el libro 42 | DELETE /books/42 | 204 No Content |
No pongas la acción en la ruta (/getBook?id=42). Hacerlo tira a la basura lo que navegadores, caches y proxies ya saben de los métodos: por ejemplo, que GET es seguro y cacheable.
Stateless (sin estado)
En REST el servidor no guarda memoria de la conversación con el cliente. Cada solicitud lleva todo lo necesario para entenderse (credenciales, parámetros), así que cualquier copia del servidor puede responderla. Esto ayuda a escalar con muchos servidores y a recuperarse de fallas. El costo: algunos datos se repiten en cada solicitud. Esto habla del estado de sesión; el servidor sigue guardando recursos como libros con normalidad.
Modelo de madurez de Richardson
- Nivel 0: una sola dirección usada como túnel para todo.
- Nivel 1: recursos separados, cada uno con su propia URL.
- Nivel 2: métodos HTTP y códigos de estado de verdad.
- Nivel 3: controles de hipermedia (HATEOAS): la respuesta le dice al cliente qué puede hacer a continuación, con enlaces.
Elegir el estado correcto
400 para una solicitud mal formada, 404 cuando el destino no existe, 409 cuando una solicitud válida choca con el estado actual (por ejemplo, un segundo libro con un ISBN que debe ser único) y 422 cuando el formato es correcto pero el contenido rompe una regla. Códigos precisos dejan que los clientes reaccionen sin leer el texto del error.
Reintentos: DELETE e Idempotency-Key
Tras un timeout el cliente no sabe si el servidor terminó el trabajo. Repetir un DELETE es seguro: el segundo puede devolver 404, y eso no rompe la idempotencia, porque el estado final es el mismo. POST no es idempotente, y repetirlo puede crear dos pedidos. La solución es una clave de idempotencia: el cliente inventa un valor único (Idempotency-Key: abc123) y lo repite en cada reintento. El servidor reconoce la repetición y devuelve el resultado de la primera ejecución. Este encabezado es una convención común, no parte del HTTP básico.
¿PUT o PATCH?
PUT reemplaza el estado por la representación que envías, así que debes mandar el recurso completo, y es idempotente. PATCH envía solo los cambios, lo que ahorra bytes y evita pisar campos que otras personas cambiaron, pero por definición no es idempotente.
Paginación
Las listas grandes se devuelven en páginas. La paginación por offset dice 'salta 3, dame 3'. Si se añaden dos elementos nuevos arriba entre dos solicitudes de página, la ventana se desplaza y recibes elementos que ya habías visto. La paginación por cursor dice 'dame 3 elementos después del id 8', así que continúa desde el último elemento visto y es estable, pero no puedes saltar directo a la página 50.
más nuevo primero: 12 11 10 9 8 7 6 5 4 3 2 1 (12 y 11 entraron después de la página 1)
la página 1 fue: 10 9 8
offset=3 : salta 12 11 10 -> devuelve 9 8 7 (9 y 8 repetidos)
before=8 : id < 8 -> devuelve 7 6 5 (nada repetido)Over-fetching y under-fetching
En REST quien decide la forma de cada respuesta es el servidor. Si da más de lo que la pantalla necesita, se desperdician bytes: over-fetching. Si da menos, el cliente debe hacer más solicitudes: under-fetching. Ejemplo: una pantalla con un autor, sus 5 libros y la editorial de cada libro (5 distintas) necesita 1 + 5 + 5 = 11 solicitudes con endpoints que devuelven solo ids. Soluciones: endpoints compuestos, un parámetro expand o GraphQL.
GraphQL: pide exactamente lo que quieres
GraphQL tiene una sola dirección y un schema tipado, una lista de todos los tipos y campos que existen. El cliente envía una query que lista los campos exactos que quiere, incluso atravesando relaciones, y la respuesta tiene la misma forma. Una solicitud puede reemplazar las varias solicitudes de REST.
query {
book(id: 7) {
title
author { name }
}
}El schema y los tipos raíz
- Todo tipo es anulable (nullable) por defecto. Un
!lo vuelve no nulo:title: String!significa que el servidor promete nunca devolver null ahí (en un argumento, el cliente está obligado a dar un valor). - Un schema tiene hasta tres tipos de operación raíz:
Query(leer),Mutation(escribir y luego leer) ySubscription(un flujo largo de eventos). SoloQueryes obligatorio. Sus campos son las puertas de entrada de la API.
Resolvers
El servidor ejecuta la query campo por campo. Para cada campo llama a una función llamada resolver, pasándole el objeto padre y los argumentos. El schema describe la forma de los datos; los resolvers dicen de dónde vienen los datos.
Errores y null
Una respuesta es un objeto con data y, si algo falló, errors. Como una operación puede tener éxito en parte, la convención habitual (JSON sobre HTTP) responde `200` incluso con errores, así que el cliente debe leer siempre errors, no solo el código HTTP.
Cuando un resolver falla, su campo se vuelve null. Si el campo es ! (no nulo), null no está permitido, así que el null sube al padre, y otra vez, hasta el primer campo anulable. Si no se encuentra ninguno, todo data se vuelve null. Por eso marcar todo con ! tiene un costo: una pequeña falla puede borrar una gran parte de la respuesta.
type User { name: String! posts: [Post!]! }
type Post { title: String! }
el title del post 2 falla:
title es String! -> el post 2 no puede ser null
[Post!]! -> la lista no admite un ítem null, así que no puede ser null
User.posts es ! -> posts no puede ser null
user(id) es anulable-> user pasa a ser null
resultado: { "data": { "user": null }, "errors": [ ... ] }Variables, aliases y orden de ejecución
- Las variables (
$term) mantienen la estructura de la query separada de los datos. Los valores viajan en un mapa JSON junto al documento y se comprueban contra el tipo declarado. Si pegaras el texto dentro de la query, un usuario podría inyectar pedazos de query. Las variables también permiten que el mismo documento se guarde en cache o se registre como persisted query. - Los aliases (
first: user(id: 1)ysecond: user(id: 2)) cambian la clave en la respuesta. Sin ellos, pedir el mismo campo dos veces con argumentos distintos se rechaza, porque ambos usarían la claveuser. - Orden. Los campos de una query pueden correr en cualquier orden, incluso en paralelo, porque no tienen efectos secundarios. Los campos del nivel superior de una mutation corren uno tras otro, en el orden en que se escribieron. Esto vale solo para el nivel superior; los subcampos corren normalmente.
El problema N+1
Como cada campo tiene su resolver, una lista de N elementos con un campo anidado hace 1 consulta para la lista y luego N consultas para el campo anidado. Con 20 posts y sus autores, son 1 + 20 = 21 consultas SQL, aunque haya solo 4 autores distintos. La solución es un DataLoader: los resolvers piden una clave al loader, el loader reúne todas las claves del paso actual, quita las repetidas y hace una sola consulta (WHERE id IN (...)). Ahora son 1 + 1 = 2. Un loader vive por una solicitud, así que nunca filtra filas a otros clientes.
- Sin batching
- 201 sentencias
- Con loader que agrupa
- 3 sentencias
Costo, profundidad y cache
Como el cliente escribe la query, el cliente también decide cuánto cuesta. Un tipo que apunta a sí mismo permite anidamiento sin fin, y el costo se multiplica: cuatro niveles de friends(first: 100) piden 100 x 100 x 100 x 100 = 100^4 = 100 millones de objetos. Contar solicitudes no basta, porque una sola solicitud puede ser abusiva. Los servidores analizan la profundidad (depth) y una complejidad estimada del documento y lo rechazan antes de ejecutarlo.
El cache es más difícil que en REST. El cache HTTP se diseñó en torno a GET y una URL por recurso, pero el GraphQL típico envía todo con POST a /graphql, y la diferencia está en el cuerpo. Salidas: enviar queries por GET, usar persisted queries identificadas por un hash en la URL, o hacer cache en el cliente por tipo e id.
- REST (libros completos)
- 27.6 kB
- GraphQL (2 campos)
- 3 kB
JSON-RPC y gRPC: llamar a una función lejana
Un RPC (Remote Procedure Call, llamada a procedimiento remoto) hace que llamar a otro computador se sienta como llamar a una función normal. No piensas en recursos ni campos: nombras una acción (book.get, sum) y le das parámetros.
JSON-RPC 2.0
Una especificación muy pequeña. Una solicitud tiene jsonrpc: "2.0", method, params opcional y un id. La respuesta tiene jsonrpc, el mismo id (para que puedas emparejar respuestas con solicitudes) y exactamente uno entre result y error, nunca los dos. Un error tiene un code entero, un message y data opcional.
{ "jsonrpc": "2.0", "method": "book.get", "params": { "id": 7 }, "id": 1 }
{ "jsonrpc": "2.0", "result": { "title": "Example" }, "id": 1 }| Código de error | Significado |
|---|---|
| -32700 | Parse error: el texto no es JSON válido |
| -32600 | Invalid Request: es JSON, pero no es un objeto de solicitud válido |
| -32601 | Method not found: el método no existe |
| -32602 | Invalid params: por ejemplo "abc" donde se espera un entero |
| -32603 | Internal error: error interno |
| -32000 a -32099 | Reservados para los errores propios del servidor. Los errores de la aplicación usan códigos fuera del rango reservado |
- Notificación (notification): una solicitud sin `id`. El servidor no debe responder nada, ni siquiera un error. Sirve para eventos y logs, pero el cliente nunca sabe si falló.
- Batch (lote): un array de solicitudes enviadas juntas. El servidor devuelve un array con una respuesta por cada solicitud que tiene
id, en cualquier orden. Las notificaciones no reciben nada. Un error en un elemento no afecta a los demás. Si el lote solo tiene notificaciones, el servidor no devuelve nada. - Un lote solo ahorra round trips para llamadas que no dependen unas de otras.
| Tema | REST | JSON-RPC |
|---|---|---|
| Eliminar el libro 42 | DELETE /books/42 | POST /rpc con {"method":"book.delete","params":{"id":42},"id":1} |
| La acción está en | el método HTTP | el cuerpo (method) |
| El resultado está en | el código de estado | result o error en el cuerpo |
| Transporte | HTTP | cualquiera (HTTP, sockets...) |
gRPC
gRPC parte de un contrato escrito en Protocol Buffers (un archivo .proto). El mismo archivo define los servicios, los mensajes y la codificación binaria compacta, en la que cada campo se identifica por un número en vez de un nombre. Un compilador genera código tipado en muchos lenguajes, así que llamar al servicio remoto parece llamar a una función local. gRPC viaja sobre HTTP/2: cada llamada es un stream (encabezados con el nombre del método, mensajes en frames DATA y un estado final en los trailers), y la multiplexación deja que muchas llamadas compartan una conexión.
message Book {
string title = 1;
int32 year = 2;
}
service Library {
rpc Get (BookId) returns (Book); // unary
rpc Upload (stream Chunk) returns (Summary); // client streaming
}| Tipo de llamada | Mensajes | Firma |
|---|---|---|
| Unary | 1 solicitud, 1 respuesta | rpc A (Req) returns (Res) |
| Server streaming | 1 solicitud, varias respuestas | rpc A (Req) returns (stream Res) |
| Client streaming | varias solicitudes, 1 respuesta | rpc A (stream Req) returns (Res) |
| Bidireccional | varias en ambos sentidos, independientes | rpc A (stream Req) returns (stream Res) |
Deadlines (plazos). El cliente dice hasta cuándo está dispuesto a esperar. Si se acaba el tiempo, la llamada termina con DEADLINE_EXCEEDED y se avisa al servidor para que deje de trabajar. En una cadena A, B, C el plazo se pasa como tiempo restante. Si A da 500 ms y B usa 200 ms, C recibe 300 ms. Si C necesita 400 ms, la llamada termina en la marca de 500 ms con DEADLINE_EXCEEDED.
t=0 A llama a B (plazo = t 500)
t=200 B llama a C (quedan 300 ms)
t=500 vence el plazo -> DEADLINE_EXCEEDED, C se cancela
(C habría terminado en t 600)- REST
- 3 round trips
- JSON-RPC
- 2 round trips
- GraphQL
- 1 round trips
WebSocket y server-sent events: el servidor habla primero
En HTTP puro el cliente siempre pregunta primero. Pero un chat, un juego o un tablero de cotizaciones necesita que el servidor envíe novedades por su cuenta. Hay cuatro maneras de hacerlo.
| Técnica | Cómo funciona | Bueno para |
|---|---|---|
| Polling | El cliente vuelve a preguntar cada pocos segundos | Cambios poco frecuentes |
| Long polling | El servidor mantiene la solicitud abierta hasta tener novedades; una solicitud por mensaje | Notificaciones simples |
| Server-sent events (SSE) | Una respuesta HTTP que nunca termina; de una sola vía, del servidor al cliente | Tableros, feeds, barras de progreso |
| WebSocket | Una conexión, de dos vías, texto o binario | Chat, juegos, colaboración |
WebSocket
Un WebSocket empieza como un GET HTTP/1.1 normal que pide un upgrade. Si el servidor acepta, responde 101 Switching Protocols, y desde entonces la misma conexión TCP habla el protocolo WebSocket. ws:// corre sobre TCP plano y wss:// corre dentro de TLS (como http:// y https://).
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: AAECAwQFBgcICQoLDA0ODw==
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: Bz3qJYTGdOe8gUSpLosEdiLKDrk=Sec-WebSocket-Acceptes un SHA-1una función que convierte cualquier texto en una huella digital corta de la clave del cliente más un texto público fijo, escrito en Base64una manera de escribir bytes usando solo letras, dígitos y algunos símbolos. Solo prueba que el servidor entendió WebSocket y respondió a esta solicitud. No autentica a nadie y no cifra nada. Eso es trabajo de TLS y de tu aplicación.- Después del handshake, los datos viajan en frames de texto (UTF-8) o binarios. Existen tres frames de control: ping y pong (un latido: '¿sigues ahí?' / 'sí') y close (iniciar una despedida ordenada con un código y un motivo).
- Masking (máscara): todo frame que viene del cliente pasa por un XOR con una clave aleatoria de 32 bits enviada en el mismo frame. Esto no oculta los datos. Protege a proxies defectuosos de ser engañados para tratar los bytes como HTTP (cache poisoning), porque no se confía en que el script de la página elija los bytes exactos. Los frames del servidor no se enmascaran.
Server-sent events
SSE es solo una respuesta HTTP normal que no termina. El servidor usa Content-Type: text/event-stream y sigue escribiendo texto. El navegador lo lee con la API EventSource. Para hablar de vuelta, el cliente hace otras solicitudes HTTP comunes.
: keep-alive <- un comentario (empieza con dos puntos)
retry: 5000 <- reconecta tras 5 segundos
id: 42
event: price
data: 11
- Campos:
data(el contenido),event(el nombre; sin él el nombre esmessage),id(la posición en el flujo) yretry. - Un evento se entrega solo cuando llega la línea vacía que cierra su bloque. Varias líneas
dataen el mismo bloque se vuelven un solo valor, unido con saltos de línea. Un bloque que aún no se cerró no se entregó. - Sin
event:se llama al handleronmessage. Conevent: updatesolo se llama a los listeners añadidos conaddEventListener('update', ...). - Si la conexión se cae,
EventSourceespera el tiempo deretryy pide de nuevo, enviando `Last-Event-ID` con el últimoidque vio. El servidor puede reanudar sin perder eventos.
Juntando todo
Ahora podemos ver cómo se apilan las piezas. Cuando abres https://ejemplo.test por primera vez, tu navegador inicia una conexión TCP, hace un handshake TLS 1.3 (SNI elige el certificado, ALPN elige h2) y luego envía solicitudes HTTP/2. Las respuestas traen Cache-Control para que la próxima vez sea más rápido. El servidor puede añadir alt-svc para que más adelante uses HTTP/3.
| Necesidad | Elige | Por qué |
|---|---|---|
| API pública y duradera, con recursos y cache HTTP | REST | Herramientas estándar y cache de GET |
| Muchas pantallas, cada una con una rebanada distinta de datos conectados | GraphQL | Una solicitud, solo los campos deseados. Exige batching y límites de costo |
| Acciones que no son recursos, o servicios internos | JSON-RPC | Lo más simple de construir, independiente del transporte |
| Llamadas rápidas entre servicios, con contrato estricto | gRPC | Binario, tipado, streaming y deadlines de HTTP/2 |
| El servidor solo empuja actualizaciones | SSE | HTTP común, reconexión automática |
| Los dos lados envían con frecuencia | WebSocket | Dos vías, poco costo por mensaje |
| Capa | Pregunta que responde | Ideas clave |
|---|---|---|
| Semántica de HTTP | ¿Qué significa el mensaje? | Métodos (seguro, idempotente), códigos de estado, encabezados, delimitación |
| Cache, cookies, CORS | ¿Qué se puede reutilizar, recordar o leer? | max-age, no-store, revalidación, Vary, SameSite, origen |
| TLS | ¿Es privado y es el servidor correcto? | Certificados, acuerdo de claves, 1 contra 2 round trips, forward secrecy |
| HTTP/1.1, 2, 3 | ¿Cómo van los mensajes al cable? | Multiplexación, HPACK/QPACK, QUIC, head-of-line blocking |
| Estilos de API | ¿Qué nombra el cliente y quién da forma a la respuesta? | Recurso, campos, procedimiento |
Errores comunes
- Pensar que
no-cacheprohíbe guardar (significa 'comprueba primero'). - Mezclar 401 (¿quién eres?) con 403 (no puedes).
- Creer que CORS protege al servidor (protege la lectura de la respuesta, en los navegadores).
- Pensar que los errores de GraphQL usan códigos 4xx o 5xx (la convención es
200conerrors). - Pensar que HTTP/3 es siempre más rápido (ayuda más con latencia alta o pérdidas).
- Olvidar que WebSocket necesita una comprobación de
Originen el servidor. - Reutilizar un número de campo de Protocol Buffers.