,

Enabling CORS in a Headless WordPress Setup

If you’re building a React, Next.js, or Vue frontend that talks to WordPress as a backend, you’ve probably seen this before:

Access to fetch at ‘https://your-wp-site.com/wp-json/wp/v2/posts’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.

That’s not WordPress being picky — it’s your browser doing its job.
Let’s break down why that happens, what’s going on under the hood, and how to fix it with a simple plugin and a few lines of JavaScript.

Table of Contents

Understanding CORS (and Why It Exists)

Modern browsers enforce the same-origin policy, which prevents JavaScript from making requests to a different domain, scheme, or port.
This rule keeps users safe but makes “headless” setups tricky — your frontend (localhost:3000) can’t directly talk to your backend (your-wp-site.com) unless the server explicitly allows it.

That’s where CORS, or Cross-Origin Resource Sharing, comes in.
It’s an HTTP-based handshake between browser and server. The browser asks:

“Can I access this resource?”

And the server replies:

“Yes — if you’re from this origin and follow these rules.”

How the Browser Handles It

When your frontend calls another origin, the browser checks what kind of request it is.

  • Simple requests (e.g. GET, standard headers) are sent directly.
  • Preflighted requests (e.g. POST with Authorization) trigger an OPTIONS check before the real call.

Enabling CORS for requests to WordPress from the outside

By default, WordPress REST API only sends very limited CORS headers — just enough for basic same-origin requests.
If your frontend app is running on another domain (like localhost:3000 or frontend.yourdomain.com), you need to explicitly tell WordPress to allow requests from that origin.

CORS support on the server side is implemented entirely through HTTP response headers.
When the browser checks whether it can talk to your API, it looks for these headers in the server’s response.

Let’s walk through what needs to be in place and then see how to do it in a plugin.

The Required Response Headers

To make your WordPress server CORS-friendly, you must return the following headers for REST API routes:

HeaderWhat It DoesExample
Access-Control-Allow-OriginTells the browser which origins (domains) are allowed to make requests.Access-Control-Allow-Origin: https://myfrontend.com
Access-Control-Allow-MethodsLists which HTTP methods are permitted for cross-origin requests.Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE
Access-Control-Allow-HeadersLists which custom headers a browser can send. Needed if your frontend uses headers like Authorization or Content-Type.Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-CredentialsIndicates whether cookies or credentials can be sent with requests. Required if using JWTs or authentication cookies.Access-Control-Allow-Credentials: true

If your server omits or mismatches any of these, the browser will block the request before your PHP code runs.

Adding These Headers in WordPress

WordPress already sends some CORS headers internally, but they’re not flexible enough for headless setups.
We can replace them with our own by hooking into the REST API response.

Create a new plugin:

  1. Inside your WordPress installation, go to wp-content/plugins
  2. Create a folder named headless-cors-allow
  3. Inside that folder, create a file named headless-cors-allow.php with this code:
<?php
/**
 * Plugin Name: Headless CORS Allow
 * Description: Enables secure CORS for headless frontends.
 * Author: Your Name
 * Version: 1.0.0
 */

defined( 'ABSPATH' ) || exit;

add_action( 'rest_api_init', function() {
    // Disable WordPress's default CORS headers.
    remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );

    // Add custom CORS headers.
    add_filter( 'rest_pre_serve_request', function( $value ) {
        $allowed_origin = 'http://localhost:3000'; // Change this to your frontend URL.

        header( "Access-Control-Allow-Origin: {$allowed_origin}" );
        header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE' );
        header( 'Access-Control-Allow-Credentials: true' );
        header( 'Access-Control-Allow-Headers: Authorization, Content-Type' );

        return $value;
    } );
}, 15 );
  1. Go to Plugins → Installed Plugins in your WordPress dashboard and activate Headless CORS Allow.

Choosing Your Allowed Origin

Always set $allowed_origin to the specific frontend domain that should communicate with WordPress, for example:

$allowed_origin = 'https://app.yourdomain.com';

Avoid using a wildcard (*) when you’re sending credentials (cookies, JWT tokens, etc.), since browsers will refuse to send authentication data unless the origin is explicitly listed.

So in short:
Enabling CORS isn’t about adding JavaScript — it’s about teaching your server to say,

“Yes, I know this frontend, and it’s allowed to talk to me.”

What happens in the browser when there’s a cross-origin request

When you make a cross-origin request, the browser evaluates it first:

  • If it’s safe, it sends it directly (simple request, no preflight)
  • If it’s complex (like authenticated requests), it performs a preflight using the OPTIONS method before the actual request.

Simple Request (No Preflight)

A simple request is one that:

  • Uses GET, HEAD, or POST
  • Only uses safe headers (Accept, Content-Type, etc.)
  • Doesn’t include credentials or custom headers
const apiBase = 'https://your-wp-site.com/wp-json/wp/v2/posts';

fetch( apiBase, {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
    },
} )
    .then( ( response ) => response.json() )
    .then( ( posts ) => console.log( '✅ Received posts:', posts ) )
    .catch( ( error ) => console.error( '❌ Fetch error:', error ) );
Loading diagram…

✅ The browser sends the request immediately since it meets all “simple” conditions.

Authenticated Request (With Preflight)

When the frontend sends a POST, PUT, or DELETE request — or includes custom headers such as Authorization — the browser doesn’t send that request to the server immediately.

Before doing so, it automatically performs a “preflight” check using the special HTTP method OPTIONS.
This invisible first request asks the server whether it’s safe and allowed to make the real call.

Think of it as the browser asking:

“Hey, WordPress — if I send a POST with this Authorization header, will you accept it?”

Only if the server responds with permission does the browser proceed to send your actual request.

We don’t have to code this step yourself — the browser inserts it automatically under the CORS specification.

Loading diagram…

For example, the following code sends a request to create a Post with Authorization Header. In our code, this looks like a single request — but under the hood, the browser is sending two:

const apiBase = 'https://your-wp-site.com/wp-json/wp/v2/posts';

fetch( apiBase, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer my-jwt-token',
    },
    body: JSON.stringify( {
        title: 'New Post from the Headless Frontend',
        status: 'draft',
    } ),
} )
    .then( ( response ) => response.json() )
    .then( ( data ) => console.log( '✅ Created:', data ) )
    .catch( ( error ) => console.error( '❌ Fetch error:', error ) );

Our JavaScript only runs one fetch(), but before that POST is sent, the browser first issues an OPTIONS request to verify that the server allows it.

The preflight response must include:

  • Access-Control-Allow-Origin — which origins are allowed
  • Access-Control-Allow-Methods — which HTTP methods are accepted
  • Access-Control-Allow-Headers — which custom headers (like Authorization) are permitted

If all of these match the browser’s expectations, it proceeds with the real POST request.
If not, it blocks the call before any data is sent — WordPress never sees it.

Why Preflight Exists

The preflight step is a security safeguard built into browsers. It prevents malicious sites from silently sending dangerous requests to other domains on a user’s behalf.

By forcing servers to explicitly declare what’s allowed, CORS ensures:

  • Sensitive requests (POST, PUT, DELETE) aren’t sent without consent
  • Only trusted origins can perform authenticated actions
  • Browsers handle security at the network level, before your code even runs

Your WordPress plugin’s CORS headers are effectively saying:

“Yes, I trust this origin — and I allow it to use these methods and headers.”

Without those headers, even valid API requests from your own frontend will fail with a CORS error.

Wrapping Up

CORS isn’t about blocking you — it’s about protecting your users.
Once your WordPress site correctly advertises which origins can talk to it, cross-domain requests work smoothly and securely.

Before we finish, some security & best practices when developing CORS related code:

  • Always whitelist specific origins, never *.
  • Only include necessary headers in Access-Control-Allow-Headers.
  • Avoid exposing private REST endpoints to public origins.
  • Use DevTools → Network → OPTIONS to confirm your preflight responses.

Now you know how your frontend can fetch content, create posts and use authentication without any “Blocked by CORS policy” errors 🦾

Leave a Reply

Navigation

About

Writing on the Wall is a newsletter for freelance writers seeking inspiration, advice, and support on their creative journey.

Discover more from JuanMa Codes

Subscribe now to keep reading and get access to the full archive.

Continue reading