Using the Fetch API in WordPress as an Alternative to jQuery $.ajax

jQuery’s $.ajax() has been the standard tool for asynchronous HTTP requests in WordPress themes and plugins for over a decade. It works, it is well-documented, and almost every WordPress developer knows it by heart. But it comes with a cost: a hard dependency on jQuery, which means an extra network request (or a larger bundle), deferred loading headaches, and code that feels increasingly out of step with modern JavaScript. The native Fetch API has been available in all browsers without a polyfill since 2017. It is promise-based, requires zero dependencies, supports async/await cleanly, and works with exactly the same WordPress AJAX endpoint — /wp-admin/admin-ajax.php — that $.ajax() targets. The PHP handler code does not change at all: you still register your action with add_action('wp_ajax_*'), you still call check_ajax_referer(), and you still return JSON with wp_send_json_success(). Only the JavaScript side changes. This article shows a direct side-by-side comparison, an async/await pattern, and the one important edge case: when you send JSON (rather than form-encoded data) in the request body, nonce verification needs a small adjustment because $_POST is empty for JSON requests.

Problem: Your WordPress theme or plugin uses jQuery's $.ajax() for every AJAX request. You want to remove the jQuery dependency or modernise the code to native JavaScript without rewriting the PHP handler.

Solution: Replace $.ajax() with the native fetch(). The WordPress AJAX endpoint accepts standard HTTP POST with application/x-www-form-urlencoded body — no jQuery required. The PHP-side code stays identical.

Side-by-side comparison — the same WordPress AJAX request, two ways:

// Before: jQuery $.ajax
jQuery.ajax( {
    url:     myData.ajaxUrl,
    method:  'POST',
    data:    { action: 'get_posts', nonce: myData.nonce },
    success: function ( response ) { console.log( response ); },
    error:   function ( xhr )      { console.error( xhr.responseText ); },
} );

// After: native Fetch API
fetch( myData.ajaxUrl, {
    method:  'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body:    new URLSearchParams( { action: 'get_posts', nonce: myData.nonce } ),
} )
    .then( function ( response ) {
        if ( ! response.ok ) { throw new Error( 'Network error: ' + response.status ); }
        return response.json();
    } )
    .then( function ( data ) { console.log( data ); } )
    .catch( function ( err ) { console.error( err ); } );

With async/await the same call is even cleaner:

async function loadPosts() {
    try {
        const response = await fetch( myData.ajaxUrl, {
            method:  'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body:    new URLSearchParams( { action: 'get_posts', nonce: myData.nonce } ),
        } );
        if ( ! response.ok ) { throw new Error( 'HTTP ' + response.status ); }
        const data = await response.json();
        console.log( data );
    } catch ( error ) {
        console.error( 'Request failed:', error );
    }
}

The PHP handler is unchanged regardless of which JS approach you use:

<?php
add_action( 'wp_ajax_get_posts',        'ajax_get_posts_handler' );
add_action( 'wp_ajax_nopriv_get_posts', 'ajax_get_posts_handler' );

function ajax_get_posts_handler() {
    check_ajax_referer( 'my_nonce_action', 'nonce' );

    $posts = get_posts( [
        'post_type'      => 'post',
        'posts_per_page' => 5,
        'post_status'    => 'publish',
    ] );

    wp_send_json_success( array_map( function ( $post ) {
        return [
            'id'    => $post->ID,
            'title' => get_the_title( $post->ID ),
            'url'   => get_permalink( $post->ID ),
        ];
    }, $posts ) );
}

If you send JSON in the body instead of form-encoded data, $_POST will be empty and check_ajax_referer() will fail. Verify the nonce manually from php://input:

<?php
add_action( 'wp_ajax_my_action', 'my_json_handler' );

function my_json_handler() {
    $data = json_decode( file_get_contents( 'php://input' ), true );

    if ( empty( $data['nonce'] ) || ! wp_verify_nonce( $data['nonce'], 'my_nonce_action' ) ) {
        wp_send_json_error( 'Invalid nonce', 403 );
    }

    $ids = array_map( 'absint', $data['ids'] ?? [] );
    wp_send_json_success( $ids );
}

NOTE: fetch() does not reject the promise on HTTP error status codes (4xx, 5xx) — only on network failures. Always check response.ok or response.status explicitly, otherwise a 403 or 500 response will silently pass to the .then() handler and response.json() will attempt to parse an error HTML page.