GraphQL vs REST en 2026: cuándo usar cada estilo de API
Equipo Arvucore
September 21, 2025 · Actualizado el August 26, 2026
15 min read
Elige REST cuando tu API expone datos estables, con forma de recurso, a muchos consumidores que no controlas, y cuando la caché HTTP importa. Elige GraphQL cuando un puñado de clientes propios (web, iOS, Android) necesita distintas porciones del mismo grafo de datos y quieres dejar de entregar un endpoint nuevo por cada pantalla. La mayoría de las empresas acaba con ambos: REST en el borde para partners, GraphQL como capa de agregación para sus propias apps.
Cómo modelan datos y peticiones GraphQL y REST
REST modela el mundo como recursos con URL. El verbo HTTP lleva la intención (GET, POST, PUT/PATCH, DELETE), y el servidor decide la forma de cada respuesta. Los clientes componen pantallas llamando a varios endpoints y uniendo los resultados.
GraphQL modela el mundo como un grafo tipado descrito por un schema. Hay un único endpoint (por convención, POST /graphql), y el cliente envía una query que nombra exactamente los campos que quiere, incluidas las relaciones anidadas. El servidor resuelve cada campo y devuelve un JSON que refleja la query.
La consecuencia práctica: en REST el servidor es dueño de la forma de la respuesta. En GraphQL lo es el cliente, dentro de los límites del schema, así que los equipos de frontend componen queries sin abrir un ticket al backend. Ese cambio de propiedad es la razón real por la que los equipos eligen GraphQL, y la razón por la que necesita más guardarraíles.
Over-fetching y under-fetching
Over-fetching es recibir campos que no necesitas: un GET /users/42 que devuelve cuarenta campos cuando la fila de una lista muestra dos. Under-fetching es lo contrario: el endpoint no devuelve suficiente, y una pantalla de perfil que necesita el usuario, sus últimos cinco pedidos y el estado de cada envío se convierte en una llamada más N más M.
REST mitiga ambos con sparse fieldsets (?fields=id,name), recursos embebidos (?include=orders) y endpoints a medida (/users/42/profile-summary). Funcionan, pero cada combinación nueva es un cambio en el backend.
GraphQL resuelve ambos de forma estructural: una query, un round trip, solo los campos solicitados. En una red móvil con cientos de milisegundos por round trip, colapsar tres llamadas secuenciales en una es una mejora visible. En una red interna rápida importa mucho menos, y por eso GraphQL rara vez compensa para tráfico entre servicios.
Caché: semántica HTTP vs cachés de cliente
Aquí REST tiene una ventaja estructural. Un GET con URL estable es cacheable por todas las capas que ya existen: caché del navegador, CDN, reverse proxy, API gateway. Cache-Control y ETag se entienden en todas partes. Si tu tráfico es mayoritariamente de lectura y público (catálogos, contenido, precios), REST más una CDN es difícil de batir en coste. Los trade-offs de cada capa se tratan en estrategias de caché con Redis, Memcached y CDN.
GraphQL, por defecto, envía peticiones POST con la query en el cuerpo, que los intermediarios no cachean. Tres técnicas recuperan la mayor parte de lo que se pierde:
- Persisted queries. El cliente registra cada query en tiempo de build y envía solo un hash (
GET /graphql?extensions={"persistedQuery":...}). La petición se convierte en un GET estable y cacheable, y el servidor rechaza queries desconocidas, lo que además sirve como control de seguridad. - Cachés normalizadas en el cliente. Apollo Client, Relay y urql guardan cada objeto devuelto por tipo e id. Una mutation que devuelve el
Product:42actualizado refresca todas las pantallas que lo muestran. Los clientes REST solo consiguen esto con trabajo extra en TanStack Query o SWR. - Cachés de respuesta y de resolver. Una caché de respuesta en el servidor indexada por hash de la operación más variables, y hints
@cacheControlpor campo que calculan el max-age de toda la respuesta a partir del campo menos cacheable.
El resumen honesto: la caché de REST es gratis y gruesa; la de GraphQL es precisa y requiere configuración. Si tu equipo no va a hacer esa configuración, gana REST.
Versionado y evolución
Las API REST suelen versionar en la URL (/v2/orders) o en una cabecera. Una nueva versión major implica ejecutar dos implementaciones en paralelo hasta que los clientes migren. Es explícito, y también es la razón por la que muchas API REST públicas se quedan en v1 durante años solo con cambios aditivos.
GraphQL desaconseja el versionado. El schema evoluciona en el sitio: añades un campo, marcas el antiguo como @deprecated(reason: "Use totalCents"), sigues el uso por campo en un schema registry y lo eliminas cuando el uso llega a cero. Como los clientes declaran exactamente qué campos usan, sabes a quién afecta una eliminación antes de hacerla. Esto funciona con clientes propios que puedes actualizar, y peor con terceros que entregan una query una vez y no vuelven a mirar, que es una razón más para mantener REST en el borde público.
En cualquier caso, contract-first compensa: un documento OpenAPI o un archivo SDL de GraphQL versionado en el repositorio, con un diff check en CI que bloquee los breaking changes.
Errores y códigos de estado
REST se apoya en el estado HTTP: 404, 403, 422, 429, 503. Proxies, SDK y monitorización los entienden sin configuración, y la RFC 9457 (Problem Details) estandariza el cuerpo del error.
GraphQL devuelve 200 para casi todo y coloca los problemas en un array errors junto a un objeto data parcial. Una petición puede tener éxito en user y fallar en user.orders. El éxito parcial encaja con los dashboards, pero rompe herramientas que dependen de códigos de estado. En la práctica:
- Reserva los códigos de transporte (
400,401,429,5xx) para peticiones malformadas, autenticación e infraestructura. - Pon los errores de dominio en el schema como resultados tipados (
union CheckoutResult = Order | InsufficientStock | PaymentDeclined) para que los clientes los manejen con tipos en lugar de interpretar cadenas de mensaje. - Usa
extensions.codepara clases de error legibles por máquina y registra el contexto completo en el servidor, como se describe en estrategias de registro y manejo de errores.
El problema N+1 y DataLoader
Un servidor GraphQL ingenuo resuelve los campos de uno en uno. Una query de 50 pedidos con su cliente llama a customer 50 veces, produciendo 51 consultas a la base de datos. Este problema N+1 es la razón más común por la que un backend GraphQL rinde peor en benchmarks que el REST al que sustituyó.
La solución es hacer batching por petición. DataLoader (la implementación de referencia del proyecto GraphQL, con ports en todos los lenguajes principales) recoge todas las llamadas customer.load(id) hechas durante un tick de la ejecución, emite un único WHERE id IN (...) y cachea los resultados durante la vida de la petición:
const customerLoader = new DataLoader(async (ids: readonly string[]) => {
const rows = await db.customers.findMany({ where: { id: { in: [...ids] } } });
const byId = new Map(rows.map((r) => [r.id, r]));
return ids.map((id) => byId.get(id) ?? null);
});
// resolver
Order: { customer: (order, _, ctx) => ctx.loaders.customer.load(order.customerId) }
Los loaders deben crearse por petición, nunca compartirse, o la fila cacheada de un usuario se filtra en la respuesta de otro. REST tiene el mismo problema dentro de un handler que recorre filas, pero es más fácil de ver en una función que repartido entre resolvers.
Seguridad: complejidad, profundidad, introspección
La seguridad de REST es por ruta: scopes, rate limits y reglas de WAF se asocian a POST /orders, y un API gateway aplica la mayor parte. GraphQL tiene una sola ruta, así que los controles se mueven a la capa de ejecución:
- Límites de profundidad. Rechaza queries anidadas más allá de, digamos, 8 niveles.
- Puntuación de complejidad. Asigna un coste por campo, multiplícalo por el tamaño de las listas (
first: 100) y rechaza o limita por encima de un presupuesto. Cobra el rate limit por coste, no por número de peticiones. - Solo persisted queries en producción. Las cadenas de query desconocidas se rechazan, así que un atacante ni siquiera puede construir una query patológica.
- Introspección desactivada para usuarios anónimos. Alimenta los explorers y también es un mapa completo de tu schema. Mantenla activa internamente y restringida en el borde público.
- Autorización a nivel de campo en resolvers o directivas (
@auth(requires: ADMIN)), porque una query puede alcanzar cualquier tipo desde cualquier punto de entrada. - Límites de batching. Limita los arrays de operaciones, o una sola petición HTTP puede llevar mil logins.
La autenticación en sí es la misma para ambos: OAuth 2.0 u OIDC en el borde, tokens de corta duración, como se describe en autenticación moderna con OAuth 2.0, JWT y zero trust.
Herramientas y ecosistema
Las herramientas de REST son universales: OpenAPI para contratos, clientes generados en cualquier lenguaje, Postman o Bruno para explorar, curl para depurar, y todos los productos de observabilidad hablan de rutas y códigos de estado. FastAPI, NestJS y Spring generan el documento OpenAPI a partir del código.
Las herramientas de GraphQL son más estrechas pero más profundas. El schema es documentación ejecutable: GraphiQL y Apollo Sandbox ofrecen autocompletado y docs inline a partir de la introspección, y GraphQL Code Generator produce hooks y clientes tipados, así que un campo renombrado rompe el build del frontend en lugar de producción. Existen servidores maduros en todos los stacks: Apollo Server, GraphQL Yoga y Mercurius (Node), graphql-java y Spring for GraphQL (JVM), Hot Chocolate (.NET), Strawberry (Python), gqlgen (Go). Opciones gestionadas como Hasura, AWS AppSync y Apollo GraphOS cambian control por velocidad. Para observabilidad, GraphQL necesita métricas por operación nombrada en lugar de por ruta; hay instrumentación OpenTelemetry para todos los servidores principales, pero tienes que activarla.
Federation vs BFF
Cuando varios equipos son dueños de varios servicios, dos arquitecturas ponen GraphQL delante de ellos. Federation (Apollo Federation, o la especificación abierta GraphQL Composite Schemas) permite que cada equipo publique un subgraph dueño de sus tipos; un router los compone en un supergraph y planifica los joins entre subgraphs mediante campos @key. Escala a nivel organizativo, pero añade un router, un schema registry y reglas de composición, y alguien tiene que ser dueño de la plataforma.
Backend for Frontend es más simple: un servicio GraphQL (o REST) por tipo de cliente, propiedad del equipo de frontend, que llama a servicios REST y gRPC internos y da forma al resultado. Sin composición, sin registry; la duplicación entre BFF es el precio de la independencia.
Elige federation cuando muchos equipos contribuyen a un grafo consumido por muchas apps. Elige un BFF cuando uno o dos equipos de frontend necesitan agregación y los equipos de backend no quieren aprender GraphQL. Ambos encajan de forma natural sobre una arquitectura de microservicios; ninguno merece la pena en un monolito con un solo cliente.
Alternativas que merece la pena nombrar: tRPC y gRPC
tRPC ofrece type safety de extremo a extremo sin schema ni generación de código: el servidor exporta un router de procedures tipadas y el cliente importa el tipo. Solo funciona cuando cliente y servidor son TypeScript en el mismo repositorio, y no es un formato de API pública. Para una app Next.js o React Native con backend TypeScript, elimina la mayor parte de las razones para adoptar GraphQL.
gRPC usa Protocol Buffers sobre HTTP/2 con clientes generados en la mayoría de lenguajes. Es rápido, fuertemente tipado y hace streaming de forma nativa, lo que lo convierte en el estándar para llamadas internas entre servicios. El soporte en navegador requiere gRPC-Web o Connect, y es poco común como API pública. El patrón habitual es gRPC entre servicios con REST o GraphQL encima.
Tabla comparativa
| Criterio | REST | GraphQL |
|---|---|---|
| Contrato | OpenAPI (opcional, externo) | Schema SDL (obligatorio, introspectable) |
| Forma de la respuesta | Definida por el servidor, por endpoint | Definida por el cliente, por query |
| Over/under-fetching | Común; mitigado con fieldsets e includes | Resuelto por diseño |
| Round trips para datos anidados | Uno por recurso | Uno por pantalla |
| Caché HTTP | Nativa (Cache-Control, ETag, CDN) |
Necesita persisted queries como GET |
| Caché de cliente | Por URL, mediante librerías | Normalizada por tipo e id |
| Versionado | URL o cabecera, versiones paralelas | Deprecación de campos, eliminación guiada por uso |
| Errores | Códigos de estado HTTP, Problem Details | Array errors, datos parciales, resultados tipados |
| Riesgo de N+1 | Dentro de los handlers, fácil de detectar | Repartido entre resolvers, necesita DataLoader |
| Modelo de seguridad | Por ruta, en el gateway | Profundidad, complejidad, persisted queries, control de introspección |
| Subida de archivos | Multipart nativo | Spec multipart o endpoint REST aparte |
| Tiempo real | SSE o WebSockets al lado | Subscriptions integradas |
| Curva de aprendizaje | Baja; universal | Moderada; conceptos nuevos para el backend |
| Ideal para | API públicas, recursos cacheables, CRUD simple | Varios clientes propios, agregación, iteración rápida de UI |
Cuándo elegir cuál: checklist por escenario
API pública para partners y terceros
- REST con OpenAPI. Los consumidores lo conocen, los SDK se generan limpiamente y la caché en CDN mantiene el coste predecible.
- Añade GraphQL solo si los partners lo piden, detrás de persisted queries y presupuestos de complejidad.
App móvil (iOS y Android) más web
- GraphQL si las pantallas agregan varios recursos y los tres clientes necesitan campos distintos. Una query por pantalla, una caché normalizada y tipos generados se amortizan rápido.
- REST si la app es sobre todo formularios y listas sobre unos pocos recursos.
Microservicios internos, servicio a servicio
- gRPC, o REST si el equipo quiere la facilidad de depuración de HTTP. GraphQL añade overhead de resolvers sin el beneficio de round trips en una red rápida.
- Si los clientes necesitan un read model unificado, pon federation o un BFF por encima de los servicios en lugar de hacer que cada uno hable GraphQL.
Dashboards de administración y herramientas internas
- GraphQL, sobre todo con Hasura o un motor similar sobre la base de datos. Los dashboards cambian constantemente, necesitan joins y filtros arbitrarios, y el renderizado con errores parciales les encaja.
- tRPC si todo el stack es TypeScript en un repositorio y hay exactamente un cliente.
Señales que apuntan a REST en cualquier escenario
- La mayor parte del tráfico son lecturas anónimas de los mismos datos.
- Debes soportar subida y descarga de archivos como operaciones de primera clase.
- El equipo no tiene experiencia con GraphQL y el plazo es corto.
Señales que apuntan a GraphQL en cualquier escenario
- El frontend espera al backend por "solo un campo más" cada sprint.
- Tres o más aplicaciones cliente leen el mismo dominio.
- Necesitas datos de uso por campo para deprecar con seguridad.
Ejemplo mínimo: un schema, una query, el equivalente en REST
Un pequeño read model de e-commerce:
type Query {
customer(id: ID!): Customer
}
type Customer {
id: ID!
name: String!
email: String!
orders(first: Int = 10): [Order!]!
}
type Order {
id: ID!
total: Money!
placedAt: DateTime!
items: [OrderItem!]!
shipment: Shipment
}
type OrderItem { sku: String! quantity: Int! product: Product! }
type Product { id: ID! name: String! imageUrl: String }
type Shipment { carrier: String! status: ShipmentStatus! eta: DateTime }
enum ShipmentStatus { PENDING IN_TRANSIT DELIVERED }
scalar Money
scalar DateTime
La query detrás de una pantalla de "historial de pedidos":
query OrderHistory($id: ID!) {
customer(id: $id) {
name
orders(first: 5) {
id
total
placedAt
shipment { status eta }
items { quantity product { name imageUrl } }
}
}
}
Una petición, una respuesta, solo los campos que la pantalla renderiza. Con persisted queries se envía como GET y se cachea como cualquier otro GET.
La misma pantalla contra una API REST convencional:
GET /customers/42
GET /customers/42/orders?limit=5
GET /orders/1001/shipment (x5, uno por pedido)
GET /products?ids=SKU-1,SKU-2,SKU-9
Son ocho peticiones en tres oleadas secuenciales, o dos si la API ofrece GET /customers/42?include=orders.shipment,orders.items.product, que es la respuesta de REST cuando el backend acepta mantener esa gramática de include. Ambos funcionan. La cuestión es qué equipo quieres que sea dueño de la forma de la respuesta.
Recomendación
Usa REST por defecto para todo lo que se expone fuera de la empresa y para el tráfico entre servicios, donde gRPC es el otro candidato. Adopta GraphQL cuando al menos dos clientes propios necesiten vistas distintas de un grafo de datos compartido, y comprométete con el paquete que viene con él: DataLoader, persisted queries, límites de profundidad y complejidad, control de introspección y un schema registry. Si el stack es TypeScript de extremo a extremo con un solo cliente, evalúa tRPC primero. En Arvucore solemos recomendar REST en el borde y un único BFF GraphQL para las apps de producto, añadiendo federation solo cuando el número de equipos que contribuyen al grafo convierte al BFF en un cuello de botella.
¿Listo para Transformar tu Negocio?
Hablemos sobre cómo nuestras soluciones pueden ayudarte a alcanzar tus objetivos. Ponte en contacto con nuestros expertos hoy mismo.
Hablar con un ExpertoTags:
Equipo Arvucore
El equipo editorial de Arvucore está formado por profesionales experimentados en desarrollo de software. Estamos dedicados a producir y mantener contenido de alta calidad que refleja las mejores prácticas de la industria e insights confiables.
Preguntas frecuentes
- ¿GraphQL es más rápido que REST?
- No por sí mismo. GraphQL reduce los round trips y el tamaño del payload para datos anidados y específicos de cada cliente, lo que ayuda en redes móviles lentas. REST suele ser más rápido y más barato para recursos simples y cacheables, porque las CDN y los navegadores cachean respuestas GET sin trabajo extra.
- ¿Cuándo debería usar GraphQL en lugar de REST?
- Usa GraphQL cuando muchos clientes distintos necesitan vistas distintas de los mismos datos, cuando las pantallas agregan varios servicios de backend, o cuando el equipo de frontend cambia sus necesidades de datos más rápido de lo que el backend puede entregar endpoints. En los demás casos, REST es el estándar más simple.
- ¿Se pueden usar GraphQL y REST juntos?
- Sí, y es habitual. Una configuración típica expone REST a partners y al público, y ejecuta una capa GraphQL (un gateway o BFF) sobre servicios REST y gRPC internos para las apps web y móviles propias.
- ¿GraphQL sustituye el versionado de APIs?
- Sustituye el versionado por URL por la evolución a nivel de campo: añades campos, marcas los antiguos con @deprecated y los eliminas cuando el uso por parte de los clientes llega a cero. Aun así necesitas un schema registry y seguimiento de uso para hacerlo con seguridad.
- ¿GraphQL es menos seguro que REST?
- Tiene una superficie de ataque distinta. Un único endpoint que acepta queries arbitrarias necesita límites de profundidad, puntuación de complejidad, persisted queries en producción e introspección desactivada para usuarios anónimos. Con esos controles es tan seguro como una API REST bien construida.
- ¿Y tRPC o gRPC en lugar de GraphQL o REST?
- tRPC encaja en un monorepo TypeScript donde el mismo equipo es dueño del cliente y del servidor. gRPC encaja en llamadas internas entre servicios donde importan el rendimiento binario y los clientes generados. Ninguno de los dos es buena opción para una API pública consumida por terceros.
Artículos relacionados

Arquitectura hexagonal en 2026: puertos, adaptadores y Clean
Hexagonal vs Clean vs Onion explicadas con un ejemplo real en TypeScript, estructura de carpetas, estrategia de tests y los errores que hunden a la mayoría de equipos.

Caché Redis en 2026: Redis vs Memcached vs CDN
Capas de caché, cache-aside vs write-through, protección contra stampede, tabla Redis vs Memcached y reglas de CDN, con checklist de decisión.

Jamstack en 2026: qué sobrevivió y qué lo reemplazó
La etiqueta Jamstack se apagó, pero sus prácticas ganaron. Estrategias de renderizado comparadas, frameworks, hosting y un checklist para saber cuándo lo estático es la opción correcta.

Arquitectura basada en eventos: sistemas resilientes y escalables
En Arvucore, exploramos cómo la arquitectura basada en eventos transforma los sistemas distribuidos modernos, permitiendo aplicaciones ágiles, resilientes y escalables. Este artículo examina los principios fundamentales, los patrones de diseño prácticos y las consideraciones operativas para la implementación de sistemas basados en eventos en entornos empresariales. Los lectores encontrarán orientación sobre opciones de arquitectura, estrategias de integración y beneficios mensurables para impulsar la agilidad empresarial y la solidez técnica en las implementaciones de producción.