Integrar la API REST de WordPress en Aplicaciones Externas

Integrar la API REST de WordPress en Aplicaciones Externas

WordPress no es solo un CMS. Desde la versión 4.7, incluye una potente API REST que lo convierte en un backend perfecto para aplicaciones modernas. Hoy vamos a explorar cómo usar WordPress como fuente de datos para aplicaciones externas: desde un simple sitio en React hasta apps móviles o cualquier plataforma que pueda hacer peticiones HTTP.

Si has pensado en construir una aplicación moderna con React, Vue, o incluso una app móvil, pero necesitas un sistema de gestión de contenido robusto, esta es tu solución. Headless WordPress está ganando terreno por una buena razón.

¿Qué es la API REST de WordPress?

La API REST de WordPress es una interfaz que permite acceder y manipular los datos de tu sitio mediante peticiones HTTP estándar. Básicamente, convierte tu WordPress en un servicio web que puede comunicarse con cualquier aplicación.

En lugar de renderizar HTML en el servidor (el WordPress tradicional), la API REST devuelve JSON puro que puedes consumir desde cualquier frontend moderno.

Ventajas principales:

  • Desacopla el frontend del backend
  • Permite crear experiencias de usuario más rápidas y dinámicas
  • Facilita aplicaciones multiplataforma (web, móvil, desktop)
  • Mejor rendimiento con frameworks modernos
  • Mayor flexibilidad en el diseño

Entendiendo los endpoints básicos

WordPress expone automáticamente varios endpoints. Si tu sitio es https://tusitio.com, la API está disponible en:

https://tusitio.com/wp-json/

Visita esa URL en tu navegador y verás un JSON con información sobre la API y los endpoints disponibles.

Endpoints principales

GET /wp-json/wp/v2/posts          - Lista de posts
GET /wp-json/wp/v2/posts/{id}     - Un post específico
GET /wp-json/wp/v2/pages          - Lista de páginas
GET /wp-json/wp/v2/categories     - Categorías
GET /wp-json/wp/v2/tags           - Etiquetas
GET /wp-json/wp/v2/media          - Archivos multimedia
GET /wp-json/wp/v2/users          - Usuarios
GET /wp-json/wp/v2/comments       - Comentarios

Tu primera petición a la API

Vamos a empezar con lo más simple: obtener posts. Puedes probarlo directamente en tu navegador:

https://tusitio.com/wp-json/wp/v2/posts

O usando JavaScript:

fetch('https://tusitio.com/wp-json/wp/v2/posts')
  .then(response => response.json())
  .then(data => {
    console.log(data);
    // Aquí tienes un array con tus posts
  })
  .catch(error => console.error('Error:', error));

Con async/await (más moderno y limpio):

async function obtenerPosts() {
  try {
    const response = await fetch('https://tusitio.com/wp-json/wp/v2/posts');
    const posts = await response.json();
    console.log(posts);
    return posts;
  } catch (error) {
    console.error('Error al obtener posts:', error);
  }
}

Parámetros de consulta útiles

La API acepta múltiples parámetros para filtrar y ordenar resultados:

Paginación

// Obtener posts de 10 en 10
fetch('https://tusitio.com/wp-json/wp/v2/posts?per_page=10&page=1')

// La respuesta incluye headers con info de paginación:
// X-WP-Total: número total de posts
// X-WP-TotalPages: número total de páginas

Ordenar resultados

// Ordenar por fecha descendente (más reciente primero)
fetch('https://tusitio.com/wp-json/wp/v2/posts?orderby=date&order=desc')

// Ordenar por título
fetch('https://tusitio.com/wp-json/wp/v2/posts?orderby=title&order=asc')

// Otras opciones: author, id, slug, modified, relevance

Filtrar por categoría o etiqueta

// Posts de una categoría específica (por ID)
fetch('https://tusitio.com/wp-json/wp/v2/posts?categories=5')

// Posts de múltiples categorías
fetch('https://tusitio.com/wp-json/wp/v2/posts?categories=5,8,12')

// Posts con una etiqueta específica
fetch('https://tusitio.com/wp-json/wp/v2/posts?tags=3')

Buscar contenido

// Buscar posts que contengan "wordpress"
fetch('https://tusitio.com/wp-json/wp/v2/posts?search=wordpress')

Seleccionar campos específicos

Para optimizar el rendimiento, puedes pedir solo los campos que necesitas:

// Solo título y contenido
fetch('https://tusitio.com/wp-json/wp/v2/posts?_fields=title,content,date')

Incluir datos relacionados

// Incluir información del autor y de las imágenes destacadas
fetch('https://tusitio.com/wp-json/wp/v2/posts?_embed')

// Esto añade un objeto _embedded con datos completos

Ejemplo práctico: Blog en React

Vamos a crear un blog simple en React que consuma la API de WordPress:

import React, { useState, useEffect } from 'react';

function Blog() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);

  const API_URL = 'https://tusitio.com/wp-json/wp/v2';

  useEffect(() => {
    fetchPosts();
  }, [page]);

  async function fetchPosts() {
    setLoading(true);
    try {
      const response = await fetch(
        `${API_URL}/posts?per_page=10&page=${page}&_embed`
      );
      
      // Obtener el número total de páginas del header
      const total = response.headers.get('X-WP-TotalPages');
      setTotalPages(parseInt(total));
      
      const data = await response.json();
      setPosts(data);
      setLoading(false);
    } catch (err) {
      setError(err.message);
      setLoading(false);
    }
  }

  // Función helper para obtener la imagen destacada
  function getFeaturedImage(post) {
    if (post._embedded && post._embedded['wp:featuredmedia']) {
      return post._embedded['wp:featuredmedia'][0].source_url;
    }
    return null;
  }

  // Función helper para limpiar el contenido HTML
  function stripHTML(html) {
    const tmp = document.createElement('div');
    tmp.innerHTML = html;
    return tmp.textContent || tmp.innerText || '';
  }

  if (loading) return <div className="loading">Cargando posts...</div>;
  if (error) return <div className="error">Error: {error}</div>;

  return (
    <div className="blog-container">
      <h1>Blog</h1>
      
      <div className="posts-grid">
        {posts.map(post => {
          const featuredImage = getFeaturedImage(post);
          const excerpt = stripHTML(post.excerpt.rendered);
          
          return (
            <article key={post.id} className="post-card">
              {featuredImage && (
                <img 
                  src={featuredImage} 
                  alt={post.title.rendered}
                  className="post-image"
                />
              )}
              
              <h2 
                dangerouslySetInnerHTML={{ __html: post.title.rendered }}
              />
              
              <p className="post-date">
                {new Date(post.date).toLocaleDateString('es-ES', {
                  year: 'numeric',
                  month: 'long',
                  day: 'numeric'
                })}
              </p>
              
              <p className="post-excerpt">{excerpt}</p>
              
              <a 
                href={`/post/${post.slug}`}
                className="read-more"
              >
                Leer más
              </a>
            </article>
          );
        })}
      </div>

      {/* Paginación */}
      <div className="pagination">
        <button 
          onClick={() => setPage(page - 1)}
          disabled={page === 1}
        >
          Anterior
        </button>
        
        <span>Página {page} de {totalPages}</span>
        
        <button 
          onClick={() => setPage(page + 1)}
          disabled={page === totalPages}
        >
          Siguiente
        </button>
      </div>
    </div>
  );
}

export default Blog;

Componente para post individual

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';

function SinglePost() {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  const { slug } = useParams();

  const API_URL = 'https://tusitio.com/wp-json/wp/v2';

  useEffect(() => {
    fetchPost();
  }, [slug]);

  async function fetchPost() {
    try {
      const response = await fetch(
        `${API_URL}/posts?slug=${slug}&_embed`
      );
      const data = await response.json();
      
      if (data.length > 0) {
        setPost(data[0]);
      }
      setLoading(false);
    } catch (err) {
      console.error('Error:', err);
      setLoading(false);
    }
  }

  if (loading) return <div>Cargando...</div>;
  if (!post) return <div>Post no encontrado</div>;

  const featuredImage = post._embedded?.['wp:featuredmedia']?.[0]?.source_url;
  const author = post._embedded?.author?.[0];

  return (
    <article className="single-post">
      {featuredImage && (
        <img 
          src={featuredImage} 
          alt={post.title.rendered}
          className="featured-image"
        />
      )}
      
      <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
      
      <div className="post-meta">
        {author && (
          <span className="author">
            Por {author.name}
          </span>
        )}
        <span className="date">
          {new Date(post.date).toLocaleDateString('es-ES')}
        </span>
      </div>
      
      <div 
        className="post-content"
        dangerouslySetInnerHTML={{ __html: post.content.rendered }}
      />
    </article>
  );
}

export default SinglePost;

Trabajando con Custom Post Types

Si has creado custom post types, también están disponibles en la API. Solo necesitas habilitarlos:

// En functions.php al registrar tu CPT
function crear_post_type_proyectos() {
    $args = array(
        // ... otros argumentos
        'show_in_rest' => true,  // ¡Esto es crucial!
        'rest_base' => 'proyectos',  // Opcional: personaliza la URL
    );
    register_post_type('proyecto', $args);
}

Ahora puedes acceder a tus proyectos en:

https://tusitio.com/wp-json/wp/v2/proyectos

Desde JavaScript:

async function obtenerProyectos() {
  const response = await fetch('https://tusitio.com/wp-json/wp/v2/proyectos');
  const proyectos = await response.json();
  return proyectos;
}

Exponer Custom Fields en la API

Por defecto, los custom fields (meta fields) no aparecen en la API. Necesitas registrarlos explícitamente:

// Registrar un meta field en la API
function registrar_meta_field_api() {
    register_rest_field('post', 'duracion_lectura', array(
        'get_callback' => function($post) {
            return get_post_meta($post['id'], 'duracion_lectura', true);
        },
        'update_callback' => function($value, $post) {
            return update_post_meta($post->ID, 'duracion_lectura', $value);
        },
        'schema' => array(
            'description' => 'Tiempo estimado de lectura en minutos',
            'type'        => 'integer',
        ),
    ));
}
add_action('rest_api_init', 'registrar_meta_field_api');

Ahora ese campo aparecerá en la respuesta JSON:

{
  "id": 123,
  "title": {...},
  "content": {...},
  "duracion_lectura": 5
}

Autenticación y operaciones protegidas

Leer contenido público no requiere autenticación, pero crear, actualizar o eliminar posts sí. WordPress ofrece varias opciones.

1. Application Passwords (recomendado)

Desde WordPress 5.6, puedes generar contraseñas de aplicación:

  1. Ve a Usuarios > Perfil
  2. Desplázate hasta «Contraseñas de aplicación»
  3. Dale un nombre y genera una contraseña

Úsala con autenticación básica:

async function crearPost(titulo, contenido) {
  const username = 'tu_usuario';
  const appPassword = 'xxxx xxxx xxxx xxxx xxxx xxxx'; // La contraseña generada
  
  const response = await fetch('https://tusitio.com/wp-json/wp/v2/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Basic ' + btoa(`${username}:${appPassword}`)
    },
    body: JSON.stringify({
      title: titulo,
      content: contenido,
      status: 'draft' // o 'publish'
    })
  });
  
  const data = await response.json();
  return data;
}

2. JWT (JSON Web Tokens)

Para aplicaciones más complejas, JWT es la opción profesional. Necesitas instalar el plugin JWT Authentication:

# Instala el plugin JWT Authentication for WP REST API

Configuración en wp-config.php:

define('JWT_AUTH_SECRET_KEY', 'tu-clave-super-secreta-aqui');
define('JWT_AUTH_CORS_ENABLE', true);

En .htaccess:

# Habilitar Authorization header
RewriteEngine on
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

Código JavaScript para autenticación JWT:

class WordPressAPI {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.token = null;
  }

  async login(username, password) {
    try {
      const response = await fetch(`${this.baseURL}/wp-json/jwt-auth/v1/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ username, password })
      });
      
      const data = await response.json();
      
      if (data.token) {
        this.token = data.token;
        localStorage.setItem('wp_token', data.token);
        return { success: true, data };
      }
      
      return { success: false, error: data.message };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async validateToken() {
    if (!this.token) {
      this.token = localStorage.getItem('wp_token');
    }
    
    try {
      const response = await fetch(`${this.baseURL}/wp-json/jwt-auth/v1/token/validate`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.token}`
        }
      });
      
      const data = await response.json();
      return data.code === 'jwt_auth_valid_token';
    } catch (error) {
      return false;
    }
  }

  async createPost(postData) {
    try {
      const response = await fetch(`${this.baseURL}/wp-json/wp/v2/posts`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.token}`
        },
        body: JSON.stringify(postData)
      });
      
      return await response.json();
    } catch (error) {
      console.error('Error creating post:', error);
      throw error;
    }
  }

  async updatePost(postId, postData) {
    try {
      const response = await fetch(`${this.baseURL}/wp-json/wp/v2/posts/${postId}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.token}`
        },
        body: JSON.stringify(postData)
      });
      
      return await response.json();
    } catch (error) {
      console.error('Error updating post:', error);
      throw error;
    }
  }

  async deletePost(postId) {
    try {
      const response = await fetch(`${this.baseURL}/wp-json/wp/v2/posts/${postId}`, {
        method: 'DELETE',
        headers: {
          'Authorization': `Bearer ${this.token}`
        }
      });
      
      return await response.json();
    } catch (error) {
      console.error('Error deleting post:', error);
      throw error;
    }
  }
}

// Uso
const api = new WordPressAPI('https://tusitio.com');

// Login
await api.login('usuario', 'contraseña');

// Crear post
const nuevoPost = await api.createPost({
  title: 'Mi nuevo post',
  content: 'Contenido del post',
  status: 'publish'
});

Crear endpoints personalizados

A veces necesitas funcionalidad específica que no ofrecen los endpoints predeterminados:

// En functions.php o en un plugin
function mi_endpoint_personalizado() {
    register_rest_route('mi-api/v1', '/datos-especiales', array(
        'methods'  => 'GET',
        'callback' => 'obtener_datos_especiales',
        'permission_callback' => '__return_true', // Público
    ));
}
add_action('rest_api_init', 'mi_endpoint_personalizado');

function obtener_datos_especiales($request) {
    // Obtener parámetros de la URL
    $parametro = $request->get_param('filtro');
    
    // Tu lógica personalizada
    $datos = array(
        'mensaje' => 'Datos especiales',
        'filtro' => $parametro,
        'items' => array(/* ... */)
    );
    
    return new WP_REST_Response($datos, 200);
}

Acceso desde JavaScript:

const response = await fetch('https://tusitio.com/wp-json/mi-api/v1/datos-especiales?filtro=activos');
const datos = await response.json();

Endpoint con autenticación

function mi_endpoint_protegido() {
    register_rest_route('mi-api/v1', '/crear-pedido', array(
        'methods'  => 'POST',
        'callback' => 'crear_pedido',
        'permission_callback' => function() {
            return current_user_can('edit_posts');
        },
    ));
}
add_action('rest_api_init', 'mi_endpoint_protegido');

function crear_pedido($request) {
    // Obtener datos del body JSON
    $params = $request->get_json_params();
    
    $producto_id = $params['producto_id'];
    $cantidad = $params['cantidad'];
    
    // Tu lógica para crear el pedido
    // ...
    
    return new WP_REST_Response(array(
        'success' => true,
        'pedido_id' => 12345,
        'mensaje' => 'Pedido creado correctamente'
    ), 201);
}

Manejo de errores y códigos de respuesta

Es crucial manejar errores correctamente:

async function fetchWithErrorHandling(url) {
  try {
    const response = await fetch(url);
    
    // Verificar si la respuesta es exitosa
    if (!response.ok) {
      if (response.status === 404) {
        throw new Error('Recurso no encontrado');
      } else if (response.status === 401) {
        throw new Error('No autorizado');
      } else if (response.status === 403) {
        throw new Error('Prohibido');
      } else if (response.status === 500) {
        throw new Error('Error del servidor');
      } else {
        throw new Error(`Error HTTP: ${response.status}`);
      }
    }
    
    const data = await response.json();
    return { success: true, data };
    
  } catch (error) {
    console.error('Error en la petición:', error);
    return { success: false, error: error.message };
  }
}

// Uso
const result = await fetchWithErrorHandling('https://tusitio.com/wp-json/wp/v2/posts/999');

if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error);
}

Optimización y mejores prácticas

1. Caché en el cliente

Implementa caché para evitar peticiones innecesarias:

class CachedAPI {
  constructor(baseURL, cacheDuration = 300000) { // 5 minutos por defecto
    this.baseURL = baseURL;
    this.cache = new Map();
    this.cacheDuration = cacheDuration;
  }

  async fetch(endpoint) {
    const cacheKey = endpoint;
    const cached = this.cache.get(cacheKey);
    
    // Si existe en caché y no ha expirado, devolverlo
    if (cached && Date.now() - cached.timestamp < this.cacheDuration) {
      console.log('Devolviendo desde caché:', endpoint);
      return cached.data;
    }
    
    // Si no, hacer la petición
    const response = await fetch(`${this.baseURL}${endpoint}`);
    const data = await response.json();
    
    // Guardar en caché
    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now()
    });
    
    return data;
  }

  clearCache() {
    this.cache.clear();
  }
}

// Uso
const api = new CachedAPI('https://tusitio.com/wp-json/wp/v2');
const posts = await api.fetch('/posts');

2. Rate limiting

Evita hacer demasiadas peticiones seguidas:

class RateLimitedAPI {
  constructor(baseURL, requestsPerSecond = 5) {
    this.baseURL = baseURL;
    this.queue = [];
    this.processing = false;
    this.delay = 1000 / requestsPerSecond;
  }

  async fetch(endpoint) {
    return new Promise((resolve, reject) => {
      this.queue.push({ endpoint, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) return;
    
    this.processing = true;
    const { endpoint, resolve, reject } = this.queue.shift();
    
    try {
      const response = await fetch(`${this.baseURL}${endpoint}`);
      const data = await response.json();
      resolve(data);
    } catch (error) {
      reject(error);
    }
    
    setTimeout(() => {
      this.processing = false;
      this.processQueue();
    }, this.delay);
  }
}

3. Pedir solo lo necesario

Usa _fields para reducir el tamaño de la respuesta:

// En lugar de obtener todo el post
const post = await fetch('https://tusitio.com/wp-json/wp/v2/posts/123');

// Pide solo lo que necesitas
const post = await fetch(
  'https://tusitio.com/wp-json/wp/v2/posts/123?_fields=id,title,content,date'
);

4. Usar _embed sabiamente

_embed añade mucha información pero aumenta el tamaño de la respuesta. Úsalo solo cuando realmente lo necesites:

// Sin _embed (más ligero)
const posts = await fetch('https://tusitio.com/wp-json/wp/v2/posts');

// Con _embed (más pesado pero incluye imágenes, autor, etc.)
const posts = await fetch('https://tusitio.com/wp-json/wp/v2/posts?_embed');

CORS y seguridad

Si tu aplicación está en un dominio diferente a tu WordPress, necesitarás configurar CORS:

// En functions.php
function habilitar_cors() {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function($value) {
        header('Access-Control-Allow-Origin: https://tu-app.com');
        header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
        header('Access-Control-Allow-Credentials: true');
        header('Access-Control-Allow-Headers: Authorization, Content-Type');
        return $value;
    });
}
add_action('rest_api_init', 'habilitar_cors');

Importante de seguridad:

  • Nunca expongas credenciales en el código del cliente
  • Usa HTTPS siempre en producción
  • Valida y sanitiza todos los datos del usuario
  • Implementa rate limiting en el servidor
  • Considera usar nonces para peticiones sensibles

Ejemplo completo: App de noticias con Next.js

// pages/index.js
import { useState, useEffect } from 'react';
import Link from 'next/link';

export async function getStaticProps() {
  const res = await fetch('https://tusitio.com/wp-json/wp/v2/posts?per_page=10&_embed');
  const posts = await res.json();
  
  return {
    props: { posts },
    revalidate: 60 // Regenerar cada 60 segundos
  };
}

export default function Home({ posts }) {
  return (
    <div className="container">
      <h1>Últimas Noticias</h1>
      
      <div className="posts-grid">
        {posts.map(post => (
          <article key={post.id} className="post-card">
            {post._embedded?.['wp:featuredmedia']?.[0] && (
              <img 
                src={post._embedded['wp:featuredmedia'][0].source_url}
                alt={post.title.rendered}
              />
            )}
            
            <h2>
              <Link href={`/post/${post.slug}`}>
                <span dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
              </Link>
            </h2>
            
            <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
          </article>
        ))}
      </div>
    </div>
  );
}

// pages/post/[slug].js
export async function getStaticPaths() {
  const res = await fetch('https://tusitio.com/wp-json/wp/v2/posts?per_page=100');
  const posts = await res.json();
  
  const paths = posts.map(post => ({
    params: { slug: post.slug }
  }));
  
  return { paths, fallback: 'blocking' };
}

export async function getStaticProps({ params }) {
  const res = await fetch(
    `https://tusitio.com/wp-json/wp/v2/posts?slug=${params.slug}&_embed`
  );
  const posts = await res.json();
  
  if (!posts.length) {
    return { notFound: true };
  }
  
  return {
    props: { post: posts[0] },
    revalidate: 60
  };
}

export default function Post({ post }) {
  const featuredImage = post._embedded?.['wp:featuredmedia']?.[0];
  
  return (
    <article className="single-post">
      {featuredImage && (
        <img src={featuredImage.source_url} alt={post.title.rendered} />
      )}
      
      <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
      
      <div className="post-meta">
        <time>{new Date(post.date).toLocaleDateString('es-ES')}</time>
      </div>
      
      <div 
        className="content"
        dangerouslySetInnerHTML={{ __html: post.content.rendered }}
      />
    </article>
  );
}

Debugging y herramientas útiles

1. Postman o Insomnia

Usa estas herramientas para probar endpoints antes de implementarlos en tu código.

2. Browser DevTools

La pestaña Network te muestra todas las peticiones, sus respuestas y tiempos.

3. WP-CLI

Verifica endpoints desde la línea de comandos:

wp rest list

4. Plugin REST API Log

Instala este plugin para ver un log de todas las peticiones a la API.

Conclusión

La API REST de WordPress es una herramienta increíblemente poderosa que transforma WordPress en un backend headless completo. Puedes construir aplicaciones modernas, rápidas y escalables mientras mantienes la facilidad de uso de WordPress para gestionar contenido.

Ya sea que estés construyendo un blog con React, una app móvil con React Native, o cualquier aplicación que necesite consumir datos, WordPress REST API es una opción sólida y madura.