How to set up a headless front with the WordPress REST API in WordPress

Contents

Overview

This tutorial shows how to set up a headless frontend with the WordPress REST API. It covers the full workflow: preparing WordPress, enabling and customizing the REST API, authentication options, exposing custom fields and media, building a frontend that reads and writes content, handling CORS and security, optimizing performance, and deployment considerations. Included are practical code examples for PHP (WordPress), JavaScript (frontend fetch and frameworks), and server configuration snippets.

Prerequisites

  • WordPress installation (any modern version 5.6 recommended REST API is built-in).
  • Access to wp-admin and server files (to install plugins, edit theme/plugin files, and update server configuration).
  • Frontend toolchain such as React (Create React App, Next.js), Vue (Nuxt), Gatsby, or a static site generator that can fetch JSON.
  • Familiarity with JavaScript and HTTP (fetch, headers, CORS).

High-level architecture

A headless WordPress architecture separates content management (WordPress MySQL) from presentation (frontend app). The frontend consumes content through the WordPress REST API endpoints at /wp-json/. Options for the frontend include client-side rendering (CSR), server-side rendering (SSR), or static generation (SSG).

Typical components

  • WordPress admin for content and media.
  • WP REST API (core endpoints custom endpoints/plugins).
  • Frontend app (React, Vue, SSG like Next.js/Gatsby/Nuxt).
  • Optional authentication (JWT, Application Passwords, OAuth, cookie auth) for protected operations.
  • CDN and caching layers for performance.

Step 1 — Prepare WordPress

Permalinks

Set pretty permalinks: WP Admin → Settings → Permalinks → select anything other than Plain. This ensures proper REST endpoints and permalinks resolution.

Essential plugins

  • WP REST API Tooling (optional): REST API debugger plugins help inspect responses.
  • Advanced Custom Fields (ACF) if structured custom fields are needed enable the ACF to REST API add-on or expose fields manually.
  • Authentication plugins for token-based flows: JWT Authentication for WP REST API, or use core Application Passwords built into WordPress.
  • Security/Access control: plugins that control endpoint visibility if required.

Step 2 — REST API basics

WordPress default endpoints include posts, pages, media, users (limited), taxonomies, and custom post types when registered with show_in_rest. Example base endpoints:

  • /wp-json/wp/v2/posts — list posts
  • /wp-json/wp/v2/pages — list pages
  • /wp-json/wp/v2/media — media items
  • /wp-json/wp/v2/categories, /tags — taxonomies

Simple read example (public GET)

Fetch latest posts from a WordPress site using fetch:

fetch(https://example.com/wp-json/wp/v2/posts?per_page=5_embed)
  .then(res =gt {
    if (!res.ok) throw new Error(Network response was not ok:    res.status)
    return res.json()
  })
  .then(posts =gt console.log(posts))
  .catch(err =gt console.error(err))

Step 3 — Authentication options

A headless frontend may only need read-only access to public content (no auth). For creating, updating, deleting content, or protected endpoints, configure authentication. Below is a summary of common methods.

Method Use case Pros Cons
Application Passwords Machine-to-machine operations basic auth with a generated password Built into WP 5.6 . Easy to use. Scoped per user. Credentials stored client/server — avoid in public clients. Use server-side or secure environments.
JWT Token-based auth for SPA/clients No cookies stateless tokens good for SPAs Requires plugin and secure key config. Token handling on client needed. Refresh mechanism required.
OAuth Third-party apps/SSO Industry standard, scopes and flows Complex to configure often overkill for single-site headless apps
Cookie (WP nonce) Same-origin SPAs, admin-like actions Works with existing WP session and nonces Requires frontend to be served from same origin or setup CORS and credentials not ideal for decoupled public sites

Application Passwords example (create post via JS on a secure server)

Create an application password in WP profile → Application Passwords. Use Basic auth header with base64 of username:app-password. Example using fetch from a secure server environment (not browser-exposed credentials).

const username = admin
const appPassword = abcd efgh ijkl mnop // 24 chars with spaces in WP UI
const auth = btoa({username}:{appPassword}) // base64

fetch(https://example.com/wp-json/wp/v2/posts, {
  method: POST,
  headers: {
    Authorization: Basic {auth},
    Content-Type: application/json
  },
  body: JSON.stringify({
    title: Headless API Post,
    status: publish,
    content: Content created via REST API
  })
})
.then(r =gt r.json())
.then(data =gt console.log(data))
.catch(err =gt console.error(err))

JWT authentication flow (overview)

  1. Install a JWT plugin and set a secure secret key in wp-config.php (JWT_AUTH_SECRET_KEY).
  2. Client POSTs credentials to /wp-json/jwt-auth/v1/token and receives a token.
  3. Client includes Authorization: Bearer lttokengt in subsequent requests.
  4. Server validates token on each request.
// wp-config.php
define(JWT_AUTH_SECRET_KEY, a-very-long-random-string-change-this)
# Request token (curl)
curl -X POST https://example.com/wp-json/jwt-auth/v1/token 
  -d username=adminpassword=MyPass

Step 4 — Expose custom data (CPTs, meta, ACF)

Custom post types and meta fields must be exposed to the REST API. Two primary mechanisms:

  • register_post_type(…, show_in_rest => true)
  • register_meta(…) or register_rest_field(…) to expose meta or custom data

Register a custom post type with REST support

add_action(init, function() {
  register_post_type(book, array(
    label => Books,
    public => true,
    show_in_rest => true,
    supports => array(title,editor,thumbnail),
    rest_base => books, // endpoint will be /wp-json/wp/v2/books
  ))
})

Expose custom meta or ACF fields

For meta fields registered with register_meta, set show_in_rest => true. For ACF, either use an ACF to REST plugin or register fields manually.

// register a meta field for posts
register_meta(post, subtitle, array(
  show_in_rest => true,
  single => true,
  type => string,
))
// add a custom field to REST response for posts
add_action(rest_api_init, function() {
  register_rest_field(post, reading_time, array(
    get_callback => function(object) {
      content = object[content][rendered] ?? 
      words = str_word_count(strip_tags(content))
      return ceil(words / 200) // minutes
    },
    schema => null,
  ))
})

Step 5 — Register custom REST routes (when needed)

Custom endpoints encapsulate business logic (aggregations, complex queries, combined data). Use register_rest_route with a permission_callback and proper sanitization.

add_action(rest_api_init, function() {
  register_rest_route(my-plugin/v1, /featured-posts, array(
    methods => GET,
    callback => function(request) {
      args = array(
        post_type => post,
        meta_key => is_featured,
        meta_value => 1,
        posts_per_page => 5,
      )
      query = new WP_Query(args)
      data = array()
      foreach (query->posts as post) {
        data[] = array(
          id => post->ID,
          title => get_the_title(post),
          excerpt => get_the_excerpt(post),
          link => get_permalink(post),
        )
      }
      return rest_ensure_response(data)
    },
    permission_callback => __return_true // make public use proper checks for sensitive data
  ))
})

Step 6 — Media handling (upload serve)

Media uploads via the REST API are handled by POSTing multipart/form-data to /wp-json/wp/v2/media with authentication. The response includes details and source URLs (often the best way to retrieve image sizes is using _embedded or media endpoint).

// upload an image (browser or server) - requires auth
const fileInput = document.querySelector(#image)
const formData = new FormData()
formData.append(file, fileInput.files[0], fileInput.files[0].name)

fetch(https://example.com/wp-json/wp/v2/media, {
  method: POST,
  headers: {
    Authorization: Basic    btoa(admin:APP_PASSWORD) // or Bearer token
  },
  body: formData
})
.then(res =gt res.json())
.then(data =gt console.log(Uploaded media:, data))
.catch(err =gt console.error(err))

Step 7 — CORS and same-origin considerations

If the frontend is served from a different origin, CORS must be allowed on the WordPress server. For public GET endpoints, enabling CORS for specific origins is straightforward. For credentialed requests (cookies, Authorization headers), set Access-Control-Allow-Credentials: true and a specific origin (not ).

# Example .htaccess snippet to allow CORS for a specific origin

  Header set Access-Control-Allow-Origin https://frontend.example.com
  Header set Access-Control-Allow-Credentials true
  Header set Access-Control-Allow-Methods GET, POST, OPTIONS, PUT, DELETE
  Header set Access-Control-Allow-Headers Content-Type, Authorization, X-WP-Nonce

For Nginx, add appropriate add_header directives. Be careful with wildcard origins and credentials — browsers will block credentialed requests if Access-Control-Allow-Origin is .

Step 8 — Frontend patterns (SSR, CSR, SSG)

Client-side rendering (CSR)

React/Gatsby/Vue apps fetch data at runtime in the browser. Simpler but may have slower initial paint and is less SEO-friendly unless hydrated meta is handled.

Server-side rendering (SSR)

Use Next.js/Nuxt to fetch data on the server at request time. Better SEO and faster initial load. The server can securely store credentials for protected API calls.

Static site generation (SSG)

Use Gatsby, Next.js static generation, or Nuxt generate to build static pages at build time. Use incremental builds or webhook triggers for content changes (e.g., trigger build when WP content updates via plugin/webhook).

Example: Next.js getStaticProps fetching WP posts

// pages/index.js (Next.js example)
export async function getStaticProps() {
  const res = await fetch(https://example.com/wp-json/wp/v2/posts?per_page=10_embed)
  const posts = await res.json()
  return {
    props: { posts },
    revalidate: 60 // ISR: re-generate at most once per minute
  }
}

export default function Home({ posts }) {
  return (
    // render posts list...
    null
  )
}

Step 9 — Security best practices

  • Never embed long-lived secrets in public client-side code.
  • Use short-lived tokens or server-side proxies for sensitive actions.
  • Limit scope of application passwords and revoke if compromised.
  • Harden WordPress: keep core, themes, and plugins updated enforce strong admin passwords and two-factor authentication.
  • Implement permission_callback for custom routes and never return sensitive data publicly.
  • Rate-limit endpoints and consider bot protections for write endpoints.

Step 10 — Performance caching

Offload heavy reads and improve performance:

  • Use server-side caching plugins that cache REST responses (e.g., object caching, full-page caching where applicable).
  • Use a CDN for media files and static assets.
  • Cache API responses on the frontend using SWR or React Query with stale-while-revalidate semantics.
  • Consider a reverse proxy or caching layer that caches GET responses from /wp-json for public content.

Troubleshooting common issues

  • 404 on REST endpoints: Set permalinks, ensure .htaccess/Nginx rules are correct.
  • 401 Unauthorized: Check Authorization header format, plugin configuration, secret keys, and CORS credentials setup.
  • CORS errors: Adjust server headers to allow the frontend origin and required headers/methods.
  • Missing custom fields: Ensure show_in_rest is true or register the fields via register_rest_field/register_meta.
  • Large media upload fails: Check PHP max_upload_size, post_max_size, and server timeouts.

Deployment considerations

  • Host WordPress where server-side PHP and MySQL perform well. For high-traffic sites, separate database and use object caching (Redis/Memcached).
  • Host frontend separately on a static host (Netlify, Vercel) or Node host for SSR.
  • Use webhooks: when content changes in WP, trigger rebuilds on SSG providers (Netlify/GitHub Actions) using a plugin or custom rest route.
  • Implement monitoring and logging for API errors and latency.

Complete checklist before going live

  1. Permalinks configured (not Plain).
  2. Required custom post types and meta exposed via REST.
  3. Authentication configured for protected endpoints.
  4. CORS and headers configured for the frontend origin(s).
  5. Media uploads tested and CDN configured.
  6. Security hardening: updates, strong credentials, least privilege.
  7. Performance: caching, CDN, asset optimization.
  8. CI/CD or webhook triggers for SSG builds configured.

Appendix — Useful code snippets

Get JWT token and use it (JavaScript)

// get token
fetch(https://example.com/wp-json/jwt-auth/v1/token, {
  method: POST,
  headers: { Content-Type: application/json },
  body: JSON.stringify({ username: admin, password: MyPass })
})
  .then(r =gt r.json())
  .then(data =gt {
    const token = data.token
    // use token
    return fetch(https://example.com/wp-json/wp/v2/posts, {
      headers: { Authorization: Bearer    token }
    })
  })
  .then(r =gt r.json())
  .then(posts =gt console.log(posts))

Register meta and make it available for REST writes

register_meta(post, subtitle, array(
  type => string,
  single => true,
  show_in_rest => array(
    schema => array(
      type => string,
      context => array(view,edit)
    )
  ),
))

Example PHP permission callback (check capability)

permission_callback => function() {
  return current_user_can(edit_posts)
}

Final notes

A headless WordPress using the REST API provides flexibility to build rich frontends while keeping WordPress as a content store. Carefully design the API surface (public vs protected), choose an authentication approach that fits the deployment model, expose only needed fields, and add caching and CDN layers for scalability. Follow the checklist and test flows: read-only, authenticated create/update/delete, media upload, and webhook-triggered rebuilds for static sites.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *