Loading data asynchronously without a full page reload is a fundamental technique in modern web development, and WordPress themes increasingly rely on it for features like live search suggestions, infinite scroll, dynamic filter results, and real-time content updates. Historically, WordPress developers reached for jQuery’s $.ajax() method to make these requests, and while jQuery is still bundled with WordPress core, the vanilla JavaScript Fetch API has been available natively in all modern browsers since 2015 and offers a cleaner, more readable syntax without any library dependency. The Fetch API uses Promises, which means asynchronous code reads linearly from top to bottom using .then() chains or the more modern async/await syntax, rather than the nested callback structure that made older AJAX code notoriously difficult to read and debug. WordPress integrates with front-end JavaScript requests through admin-ajax.php, the built-in AJAX handler, or through the REST API introduced in WordPress 4.7. The REST API is the preferred approach for new development: it returns clean JSON responses, handles authentication through standard HTTP methods, requires no custom PHP endpoint registration for built-in data types, and separates front-end and back-end concerns cleanly. For custom data that does not fit the standard REST API routes, the wp_ajax_* and wp_ajax_nopriv_* action hooks still work perfectly and require registering a PHP handler function that echoes a JSON response and exits. The correct way to pass the ajaxurl or a nonce to your JavaScript in WordPress is through wp_localize_script(), which attaches a PHP-generated JavaScript object to an enqueued script without inline script tags scattered through templates. Nonces are essential for AJAX handlers that modify data — they verify that requests originated from your own front end rather than from cross-site request forgery attacks. The example below demonstrates a fetch call to a custom WordPress AJAX handler that returns a JSON list of recent posts, following the same enqueue pattern covered in our post on correctly enqueueing scripts in WordPress. It also applies the debounce technique from debouncing user input to prevent flooding the AJAX endpoint.
Problem: You need to load or submit data from a WordPress theme without a full page reload, without depending on jQuery.
Solution: Register a PHP AJAX handler and call it with the Fetch API. Add to functions.php:
<?php
// Register AJAX handler (fires for both logged-in and logged-out users)
add_action( 'wp_ajax_ha_get_recent_posts', 'ha_get_recent_posts_handler' );
add_action( 'wp_ajax_nopriv_ha_get_recent_posts', 'ha_get_recent_posts_handler' );
function ha_get_recent_posts_handler() {
check_ajax_referer( 'ha_fetch_nonce', 'nonce' );
$posts = get_posts( array(
'numberposts' => 5,
'post_status' => 'publish',
) );
$data = array_map( function( $post ) {
return array(
'id' => $post->ID,
'title' => get_the_title( $post->ID ),
'url' => get_permalink( $post->ID ),
);
}, $posts );
wp_send_json_success( $data );
}
// Pass ajaxurl and nonce to the script
add_action( 'wp_enqueue_scripts', 'ha_enqueue_fetch_script' );
function ha_enqueue_fetch_script() {
wp_enqueue_script( 'ha-fetch', get_template_directory_uri() . '/js/fetch.js', array(), '1.0.0', true );
wp_localize_script( 'ha-fetch', 'haAjax', array(
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ha_fetch_nonce' ),
) );
}
Then in your js/fetch.js file:
async function loadRecentPosts() {
const formData = new FormData();
formData.append( 'action', 'ha_get_recent_posts' );
formData.append( 'nonce', haAjax.nonce );
try {
const response = await fetch( haAjax.url, {
method: 'POST',
body: formData,
} );
const json = await response.json();
if ( json.success ) {
json.data.forEach( function( post ) {
console.log( post.title, post.url );
} );
}
} catch ( error ) {
console.error( 'Fetch error:', error );
}
}
document.addEventListener( 'DOMContentLoaded', loadRecentPosts );
NOTE: wp_send_json_success() wraps your data in a {"success":true,"data":{...}} envelope and sets the correct Content-Type: application/json header automatically, then calls wp_die() to terminate the request cleanly. Always call check_ajax_referer() at the top of handlers that return sensitive data or modify the database — omitting it leaves your endpoint open to cross-site request forgery. For read-only public data, the WordPress REST API at /wp-json/wp/v2/posts is even simpler — no PHP handler required.