La revolución “WordPress headless” ha madurado. En 2026, la pregunta ya no es si debería desacoplar su frontend, sino cómo debería obtener sus datos. La elección está entre la API REST nativa del núcleo de WordPress y GraphQL, que en WordPress casi siempre significa el plugin WPGraphQL.
La decisión depende ahora de requisitos de ingeniería que se pueden medir: cuántas peticiones necesita una página, cuánto JSON tiene que procesar el navegador, cómo se cachean las respuestas en el edge, de dónde obtiene el frontend sus tipos y quién mantiene el código adicional. Ambas opciones son maduras. Esta guía las compara en esos puntos, con cada nombre de API, ajuste y valor por defecto contrastado con el REST API Handbook y la documentación de WPGraphQL.
1. Diferencias entre la API REST y GraphQL en WordPress
REST (endpoint-driven)
GET /wp-json/wp/v2/posts/101
GET /wp-json/wp/v2/users/4
GET /wp-json/wp/v2/categories?post=101
= several HTTP requests, each with a fixed response shape
GraphQL (document-driven)
POST /graphql
query SinglePost {
post(id: "101", idType: DATABASE_ID) {
title
date
author { node { name } }
categories { nodes { name slug } }
}
}
= one HTTP request, the client names every field it wantsLa API REST se basa en recursos fijos. Una petición a /wp-json/wp/v2/posts devuelve la representación completa de la entrada: contenido renderizado, extracto, GUID, estado de comentarios, enlaces y más. WPGraphQL invierte esto. El cliente envía un documento de consulta y el servidor devuelve solo los campos que ese documento nombra.
Over-fetching y under-fetching en la API REST
El equipo de WPGraphQL describe el problema de REST como over-fetching y under-fetching a la vez: el endpoint de entradas devuelve IDs de categorías, IDs de etiquetas, contenido y extracto que una vista de listado no necesita, y aun así le falta el nombre y el avatar del autor, lo que obliga a hacer más peticiones (WPGraphQL vs WP REST API).
El núcleo tiene dos respuestas a esto, y a menudo se pasan por alto. El parámetro global _fields devuelve solo un subconjunto de campos, por ejemplo ?_fields=id,title,link, y desde WordPress 5.3 admite propiedades anidadas, como una sola clave de meta. El parámetro _embed incluye los recursos enlazados (autor, imagen destacada, términos) en la misma respuesta, y desde WordPress 5.4 se puede limitar a relaciones de enlace concretas (Global parameters). Con ambos, un listado vía REST puede acercarse al payload de GraphQL. Lo que REST sigue sin tener es un schema tipado contra el que el cliente pueda consultar.
2. Ventajas y desventajas de la API REST de WordPress
La API REST de WordPress lleva una década en el núcleo y es más robusta que nunca.
- La ventaja: funciona de serie. No requiere plugins adicionales para la funcionalidad básica. Es altamente cacheable a nivel HTTP con CDN estándar, porque cada recurso tiene su propia URL.
- La desventaja: over-fetching por defecto. Si solo necesita el título y el slug de una entrada y olvida
_fields, REST envía el objeto completo de la entrada, con contenido y metadatos. Eso desperdicia ancho de banda, sobre todo en conexiones móviles.
La API REST usa JSON para peticiones y respuestas, y cualquier lenguaje capaz de hacer peticiones HTTP y leer JSON puede usarla (REST API Handbook). Por eso casi todas las plataformas de integración, conectores de CRM y consumidores de webhooks hablan REST en primer lugar.
3. Ventajas y desventajas de WPGraphQL
GraphQL (generalmente implementado mediante WPGraphQL) permite al desarrollador frontend definir la forma de la respuesta.
- La ventaja: una petición, exactamente los datos que quiere. Puede obtener una entrada, sus categorías, las 3 últimas entradas del autor y la configuración del sitio en un único viaje de red. El schema está fuertemente tipado y cada campo declara su tipo.
- La desventaja: requiere un plugin adicional que hay que mantener. La complejidad de las consultas puede provocar a veces búsquedas lentas en la base de datos si no se optimiza con un object cache (Redis).
Los datos del plugin, leídos en el directorio de plugins de wordpress.org el 14 de octubre de 2026: WPGraphQL 2.24.0, requiere WordPress 6.0 y PHP 7.4, probado hasta WordPress 7.0.7, más de 30.000 instalaciones activas. La página del plugin indica que WPGraphQL está en camino de convertirse en un Canonical Plugin de WordPress.org. Sigue Semantic Versioning, así que un cambio de versión mayor indica cambios incompatibles y conviene leer las notas de la versión antes de actualizar.
4. Cómo resuelve WPGraphQL el problema N+1
Un resolver ingenuo para “las últimas 20 entradas y el nombre de cada autor” ejecuta una consulta para las entradas y luego una consulta por autor. Ese es el problema N+1. WPGraphQL lo evita con el patrón DataLoader: durante la resolución reúne los IDs que necesita (todos los valores de post_author, por ejemplo) y los carga en un solo lote. En el propio ejemplo de la documentación de WPGraphQL, las consultas SQL para los autores de cinco entradas bajan de cinco a una (WPGraphQL vs WP REST API).
SELECT * FROM wp_users WHERE ID IN (2, 5, 8, 14);Además, WPGraphQL tiene una Model Layer: un conjunto de clases PHP que envuelven los objetos de WordPress y comprueban los campos sensibles, como la dirección de correo de un usuario, antes de devolverlos (WPGraphQL concepts).
Cómo ampliar el schema de WPGraphQL en PHP
<?php
declare(strict_types=1);
add_action('graphql_register_types', function (): void {
register_graphql_object_type('PerformanceMetrics', [
'description' => __('Core Web Vitals medidos de una entrada', 'wppoland'),
'fields' => [
'lcpSeconds' => ['type' => 'Float'],
'inpMilliseconds' => ['type' => 'Int'],
'clsScore' => ['type' => 'Float'],
],
]);
register_graphql_field('Post', 'cwvMetrics', [
'type' => 'PerformanceMetrics',
'resolve' => function ($post): ?array {
$raw = get_post_meta($post->databaseId, '_wppoland_cwv_data', true);
if (!is_array($raw)) {
return null;
}
return [
'lcpSeconds' => isset($raw['lcp']) ? (float) $raw['lcp'] : null,
'inpMilliseconds' => isset($raw['inp']) ? (int) $raw['inp'] : null,
'clsScore' => isset($raw['cls']) ? (float) $raw['cls'] : null,
];
},
]);
});5. Cómo crear un endpoint REST personalizado en WordPress
El patrón del núcleo es register_rest_route() en el hook rest_api_init, o una clase que extiende WP_REST_Controller. Las reglas del handbook que importan en producción (Adding custom endpoints):
- Desde WordPress 5.5, una ruta registrada sin
permission_callbackgenera un aviso_doing_it_wrong. Para un endpoint público, pase__return_truede forma explícita. - Los namespaces deben seguir el patrón
vendor/v1, y la versión sube cuando se rompe la compatibilidad. - Los errores son un
WP_Errorcon el código HTTP en la clavestatusde sus datos, por ejemplo400para una entrada no válida. - Cuando envuelva un callback existente, pase su valor de retorno por
rest_ensure_response().
<?php
declare(strict_types=1);
class Wppoland_Metrics_Controller extends WP_REST_Controller
{
protected $namespace = 'wppoland/v1';
protected $rest_base = 'metrics';
public function register_routes(): void
{
register_rest_route($this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_item'],
'permission_callback' => '__return_true',
'args' => [
'id' => ['type' => 'integer', 'required' => true],
],
],
'schema' => [$this, 'get_public_item_schema'],
]);
}
public function get_item($request)
{
$post = get_post((int) $request['id']);
if (!$post || $post->post_status !== 'publish') {
return new WP_Error('rest_post_invalid_id', __('No encontrado.', 'wppoland'), ['status' => 404]);
}
return rest_ensure_response([
'id' => $post->ID,
'title' => get_the_title($post),
]);
}
public function get_item_schema(): array
{
return [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'wppoland_metrics',
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'title' => ['type' => 'string'],
],
];
}
}
add_action('rest_api_init', function (): void {
(new Wppoland_Metrics_Controller())->register_routes();
});Las propiedades se declaran sin tipo a propósito: WP_REST_Controller declara $namespace y $rest_base sin tipo, y PHP rechaza una redeclaración tipada en una clase hija. Para el recorrido completo del controlador, consulte nuestra guía sobre endpoints personalizados de la API REST.
6. Es GraphQL más rápido que REST en WordPress
En 2026, el rendimiento no es solo velocidad; es eficiencia.
- Latencia de red: GraphQL gana en páginas complejas. Al agrupar varios datos en una sola petición, elimina los viajes secuenciales que necesita una página REST con entrada, autor, términos y entradas relacionadas.
- Estrategia de caché: REST gana en facilidad de uso. Como cada endpoint es una URL única, puede cachearlo globalmente en Cloudflare con poco esfuerzo. GraphQL necesita peticiones GET o persisted queries para lograr resultados de edge caching similares.
- CPU del servidor: REST suele ser más ligero para un único recurso sencillo. GraphQL tiene que analizar y validar el documento de consulta antes de resolver nada, y por eso un object cache importa más en un sitio con GraphQL.
El equipo de WPGraphQL publicó una medición: obtener 100 entradas costó 335 kb y 7,91 s desde el endpoint REST, frente a 6,4 kb y 67 ms desde WPGraphQL (WPGraphQL vs WP REST API). Tómelo como la prueba del propio fabricante con la salida REST por defecto. Una petición REST con _fields reduce buena parte de la diferencia de payload, así que mida sus propias páginas antes de decidir solo por este argumento.
7. Cómo cachear GraphQL en la CDN con persisted queries
Las consultas GraphQL se envían normalmente como HTTP POST con la consulta en el cuerpo, y una CDN no cachea POST por URL. La propia guía de WPGraphQL indica que POST es el método habitual y que GET tiene un soporte limitado (Interacting with WPGraphQL). El caché en el edge lo gestiona el plugin complementario WPGraphQL Smart Cache (versión 2.3.3, más de 6.000 instalaciones activas, ahora parte del monorepo de WPGraphQL, según su página del plugin).
Cómo funciona, según la documentación de Smart Cache:
- Caché de red: para aprovecharla, envíe peticiones HTTP GET para datos públicos. Las respuestas llevan una cabecera
X-GraphQL-Keyscon etiquetas de todo lo que tocó la consulta. Cuando cambia el contenido, Smart Cache envía eventos de purga y el alojamiento elimina los documentos cacheados con esas claves. La guía cita WP Engine y Pantheon como proveedores compatibles. Elmax-agepor defecto es de 600 segundos, ajustable en la configuración de Cache del plugin (Network cache). - Persisted queries: un documento de consulta se guarda en el servidor en un tipo de contenido no público
graphql_documenty se referencia mediante un ID de consulta. El slug del documento es el hash SHA-256 de la consulta, y un cliente solo puede registrar un nuevoqueryIdsi coincide con ese hash. Después, los clientes envían peticiones GET cortas como?queryId=getPostById&variables={...}. El plugin funciona con Apollo Persisted Query Link (Persisted queries). - Listas de permitidos y denegados: en la configuración de consultas guardadas puede elegir entre un endpoint totalmente público, permitir solo determinadas consultas guardadas o denegar algunas concretas. Los usuarios con
manage_optionsquedan exentos.
Con peticiones GET, persisted queries y purga por etiquetas, una respuesta GraphQL se cachea en el edge de forma muy parecida a una URL REST. Sin ellas, el caché de GraphQL recae en el object cache de WordPress.
8. API batch de REST en WordPress frente al batching de GraphQL
WordPress 5.6 añadió un framework de batch en /wp-json/batch/v1. Acepta un POST con varias subpeticiones, 25 por defecto (filtro rest_get_max_batch_size). No admite peticiones GET, así que está pensado para escrituras, y cada ruta debe habilitarlo con allow_batch al registrarse. El modo require-all-validate rechaza el lote entero si alguna petición falla la validación (REST API batch framework in WordPress 5.6).
Limitaciones del batching en la API REST
- Sin composición de datos: las subpeticiones se ejecutan como operaciones separadas. No puede usar el resultado de la petición A (un ID de autor) en la petición B dentro del mismo lote.
- Orientado a escrituras: sin GET, el endpoint de batch no ayuda en absoluto a un frontend con muchas lecturas.
- No cacheable en el edge: es un POST, así que una CDN envía cada lote al origen.
GraphQL resuelve los datos relacionados dentro de una sola consulta por diseño, y WPGraphQL también acepta consultas por lotes. Su documentación de seguridad señala que puede limitar o desactivar las consultas por lotes en la página de ajustes de GraphQL.
9. Límites de paginación en REST y GraphQL
La API REST limita per_page a 100 registros y devuelve dos cabeceras con cada colección: X-WP-Total (total de registros) y X-WP-TotalPages (total de páginas) (Pagination). Un cliente que necesita 450 entradas hace cinco peticiones.
WPGraphQL usa conexiones basadas en cursores (first, after, pageInfo { hasNextPage endCursor }). Limita los elementos por página de conexión a 100 en la mayoría de las conexiones, y el filtro graphql_connection_max_query_amount cambia ese límite (WPGraphQL security). Súbalo solo junto con el límite de profundidad y el caché, porque las páginas más grandes abaratan el envío de consultas costosas.
10. Autenticación en la API REST de WordPress y en WPGraphQL
La API REST tiene dos métodos integrados (Authentication):
- Autenticación por cookie para el código que se ejecuta en el escritorio con la sesión iniciada. Una petición manual debe enviar un nonce para la acción
wp_rest, normalmente en la cabeceraX-WP-Nonce. Sin el nonce, la petición se trata como no autenticada, aunque el usuario haya iniciado sesión. - Application Passwords, incluidas en el núcleo desde WordPress 5.6. Usan Basic Auth y solo funcionan sobre HTTPS.
WPGraphQL sigue los permisos de WordPress: todo lo que WordPress expone públicamente es público en GraphQL, y los datos que requieren autenticación en WordPress requieren una petición GraphQL autenticada. Los usuarios sin entradas publicadas quedan excluidos de las consultas públicas de usuarios, y campos como el correo de un usuario requieren un permiso específico (WPGraphQL security). Los flujos de inicio de sesión basados en tokens (JWT y similares) vienen de extensiones, no del plugin principal.
11. Cómo proteger la API REST y GraphQL en WordPress
| Medida | WPGraphQL | API REST |
|---|---|---|
| Descubrimiento del schema | La introspection pública está desactivada por defecto; actívela en los ajustes de GraphQL solo si la necesita | El índice /wp-json/ lista las rutas; exija autenticación con rest_authentication_errors si el sitio no tiene consumidores públicos |
| Profundidad de consulta | Las instalaciones nuevas limitan la profundidad a 15; filtro graphql_query_depth_max | No aplica, la forma de la respuesta es fija por endpoint |
| Coste de consulta | Los límites de complejidad basados en coste aún no están en el plugin (seguimiento en el issue #3922) | No aplica |
| Tamaño de página | 100 elementos por página de conexión por defecto | per_page limitado a 100 |
| Batching | Las consultas por lotes se pueden limitar o desactivar | Las rutas deben habilitarlo con allow_batch |
| Autenticación de escrituras | Permisos de WordPress; tokens mediante extensiones | Cookie más nonce, o Application Passwords sobre HTTPS |
Fuentes: WPGraphQL security, REST API Handbook FAQ. La forma que propone el handbook para bloquear el acceso anónimo a REST es una comprobación is_user_logged_in() en el filtro rest_authentication_errors. La limitación de peticiones (rate limiting) corresponde al edge (WAF o CDN) en ambas API, porque ni el plugin ni el núcleo la incluyen.
12. Generar tipos TypeScript desde WPGraphQL
Como el schema de GraphQL está tipado y admite introspection, herramientas como GraphQL Code Generator pueden leer el schema y las consultas del frontend y generar tipos TypeScript para el resultado de cada consulta. Un campo que pasa a ser nullable o desaparece del schema hace fallar la comprobación de tipos en el editor y en CI, antes de llegar a producción.
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { print } from 'graphql';
export async function executeGraphQL<TData, TVariables>(
document: TypedDocumentNode<TData, TVariables>,
variables?: TVariables
): Promise<TData> {
const response = await fetch(import.meta.env.WP_GRAPHQL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: print(document), variables }),
});
if (!response.ok) throw new Error(`GraphQL HTTP ${response.status}`);
const { data, errors } = await response.json();
if (errors?.length) throw new Error(errors[0].message);
return data;
}La API REST publica un JSON Schema para cada ruta, así que también ahí se pueden generar tipos, pero hay que montarlos endpoint por endpoint y el servidor no valida la selección de campos de su cliente.
13. Gestión de errores en la API REST y GraphQL
Un error REST es un WP_Error serializado como JSON con code, message y data.status, y el estado HTTP de la respuesta coincide con data.status. Si la petición falla, toda la respuesta es un error. Eso es predecible para la monitorización, pero una página construida con una sola llamada REST no puede mostrar la mitad de sus datos.
GraphQL puede devolver datos parciales. La respuesta lleva tanto data como errors, y cada error tiene un path que apunta al campo que falló:
{
"data": { "post": { "title": "Arquitectura de WordPress headless", "relatedPosts": null } },
"errors": [
{ "message": "Internal server error", "path": ["post", "relatedPosts"] }
]
}El frontend puede mostrar el artículo y un contenido alternativo solo para el bloque que falló. La otra cara: una petición GraphQL que falló en parte sigue devolviendo HTTP 200, así que la monitorización tiene que leer el array errors, no solo los códigos de estado.
14. Mutations de GraphQL y optimistic UI en WordPress
Las escrituras en GraphQL son mutations. Una mutation cambia datos y devuelve el objeto actualizado en la misma respuesta, de modo que el cliente puede actualizar su estado local sin una segunda petición. El núcleo de WPGraphQL incluye mutations para entradas, términos, usuarios y comentarios, por ejemplo createComment:
mutation AddComment($input: CreateCommentInput!) {
createComment(input: $input) {
success
comment { id content date }
}
}Esto simplifica la optimistic UI: la interfaz muestra el nuevo comentario de inmediato y solo lo revierte si la mutation devuelve un error. Las mutations del carrito de WooCommerce no forman parte del núcleo de WPGraphQL; vienen de una extensión aparte. Con REST, el mismo flujo es un POST seguido de la lectura del recurso devuelto, lo que funciona igual de bien para escrituras individuales.
15. GraphQL o REST para agentes de IA y la Abilities API
Los agentes de IA y los clientes MCP ya son consumidores habituales de API. La introspection de GraphQL permite a un agente leer el schema y ver qué tipos y argumentos existen, pero WPGraphQL mantiene la introspection pública desactivada por defecto, así que un agente necesita acceso autenticado o un schema exportado.
El núcleo de WordPress tomó otro camino. La Abilities API, en el núcleo desde WordPress 6.9 (2 de diciembre de 2025), registra operaciones con un schema de entrada y de salida, un permission callback y un execute callback, y las expone a través de los endpoints REST wp-abilities/v1 y del MCP Adapter (Abilities API in WordPress 6.9). Para agentes que actúan sobre un sitio, esta capa basada en REST es la vía nativa. Más detalles en nuestra guía sobre la WordPress Abilities API.
16. Probar las API de WordPress con GraphiQL y Postman
- REST: familiar para prácticamente todo desarrollador. Puede probar endpoints al instante en un navegador o en herramientas como Postman. Añada
?_envelopecuando un cliente no pueda leer cabeceras: la respuesta lleva entonces las cabeceras y el código de estado en el cuerpo. - GraphQL: WPGraphQL incluye GraphiQL, un IDE en el navegador dentro del escritorio de WordPress que permite recorrer todo el schema, ver los campos disponibles y probar consultas y mutations con autocompletado. Para los equipos frontend es una ganancia real de productividad.
Para evitar regresiones, compare en CI las consultas del frontend con el schema en cada merge, de modo que un campo eliminado o con otro tipo haga fallar el build. En el lado REST, valide las respuestas contra el JSON Schema de cada ruta.
17. Cómo migrar de la API REST a WPGraphQL sin tiempo de inactividad
- Convivencia: instale WPGraphQL en staging con
wp plugin install wp-graphql --activatey mapee cada tipo de contenido personalizado y cada taxonomía en el schema. REST sigue sirviendo producción. - Primero las páginas de solo lectura: pase las páginas de archivo y de listado a consultas GraphQL, con tipos generados, y compare el payload y el tiempo de respuesta con la versión REST.
- Páginas de detalle y caché: pase las vistas individuales y después añada Smart Cache con peticiones GET o persisted queries si su alojamiento admite caché de red.
- Limpieza de la exposición REST: mantenga las rutas REST que usan webhooks, automatizaciones e integraciones, y exija autenticación donde ya no quede ningún consumidor público.
Para el paso 2, empiece con consultas sencillas y aumente la complejidad poco a poco:
# Consulta básica
query {
posts(first: 10) {
nodes {
title
slug
date
}
}
}
# Consulta avanzada con filtros
query ArticulosPorCategoria($slug: String!) {
posts(where: { categoryName: $slug }) {
nodes {
title
excerpt
featuredImage {
node {
sourceUrl(size: MEDIUM)
}
}
}
}
}Integración en el frontend, con Astro:
---
const response = await fetch('https://su-wp.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `query { posts(first: 10) { nodes { title slug } } }`
}),
});
const { data } = await response.json();
---
{data.posts.nodes.map(post => (
<article>
<h2><a href={`/blog/${post.slug}`}>{post.title}</a></h2>
</article>
))}18. Tabla comparativa REST vs. GraphQL
| Característica | API REST | GraphQL (WPGraphQL) |
|---|---|---|
| Instalación | Nativa (integrada) | Necesita plugin |
| Obtención de datos | Endpoints fijos | Flexible, definida por el cliente |
| Over-fetching | Por defecto, se reduce con _fields | Lo evita la consulta |
| Datos relacionados | Peticiones adicionales o _embed | Una consulta |
| Caché | Fácil (basada en URL) | Peticiones GET o persisted queries |
| Tipado | JSON Schema por ruta, no impuesto en el cliente | Schema fuertemente tipado |
| Límite por página | 100 por página | 100 por página de conexión |
| Ideal para | Apps sencillas, webhooks, integraciones | Apps JS complejas, headless |
Cuándo usar GraphQL en lugar de REST en WordPress
Si está construyendo un sitio moderno y desacoplado con React, Astro o Next.js, GraphQL suele ser la mejor opción. La posibilidad de pedir exactamente lo que se necesita, con tipos generados, da un frontend más ligero y más fácil de mantener.
Sin embargo, no ignore REST. Para integraciones sencillas, apps móviles que solo necesitan un feed de entradas, automatización interna, webhooks y agentes de IA que usan la Abilities API, la API REST nativa sigue siendo la forma más rápida de resolver el trabajo, sin ningún plugin que mantener. Muchos sitios en producción usan ambas: GraphQL para el frontend público y REST para las integraciones.
Conozca más sobre la migración de sitios a Astro y Next.js en WPPoland.
Todavía obtiene el objeto completo de la entrada solo para un título. Añada _fields hoy y planifique el paso a GraphQL cuando el frontend se le quede pequeño.
Recursos relacionados
- Desarrollo WordPress profesional - Arquitecturas API empresariales
- Migración a Astro y Next.js - Frontends modernos con GraphQL
- Optimización de velocidad WordPress - Rendimiento de APIs
- Desarrollo WooCommerce - APIs de comercio electrónico
- SEO, GEO y AEO WordPress - SEO para sitios headless







